From 60ec7b842443c4902a2de3e0abe502fc537ad8eb Mon Sep 17 00:00:00 2001 From: Philip Hyunsu Cho Date: Wed, 24 Jan 2024 13:02:39 -0800 Subject: [PATCH 01/16] Throw error for 32-bit archs (#10005) --- CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index dbfa1cdc2..4f240e806 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,6 +39,9 @@ elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") message(FATAL_ERROR "Need Clang 9.0 or newer to build XGBoost") endif() endif() +if(CMAKE_SIZE_OF_VOID_P EQUAL 4) + message(FATAL_ERROR "XGBoost does not support 32-bit archs. Please use 64-bit arch instead.") +endif() include(${xgboost_SOURCE_DIR}/cmake/PrefetchIntrinsics.cmake) find_prefetch_intrinsics() From c8f5d190c6b0d444ee71663ce1f053892756345d Mon Sep 17 00:00:00 2001 From: Philip Hyunsu Cho Date: Wed, 24 Jan 2024 22:54:21 -0800 Subject: [PATCH 02/16] [CI] Stop Windows pipeline upon a failing pytest (#10003) --- src/data/device_adapter.cuh | 8 +++----- src/data/ellpack_page.cu | 4 ++-- src/data/simple_dmatrix.cuh | 4 ++-- tests/buildkite/test-win64-gpu.ps1 | 2 ++ tests/python-gpu/test_gpu_prediction.py | 1 + tests/python-gpu/test_gpu_with_sklearn.py | 2 +- 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/data/device_adapter.cuh b/src/data/device_adapter.cuh index 67ceb92f2..a5156f585 100644 --- a/src/data/device_adapter.cuh +++ b/src/data/device_adapter.cuh @@ -260,12 +260,10 @@ bool NoInfInData(AdapterBatchT const& batch, IsValidFunctor is_valid) { auto counting = thrust::make_counting_iterator(0llu); auto value_iter = dh::MakeTransformIterator(counting, [=] XGBOOST_DEVICE(std::size_t idx) { auto v = batch.GetElement(idx).value; - if (!is_valid(v)) { - // discard the invalid elements. - return true; + if (is_valid(v) && isinf(v)) { + return false; } - // check that there's no inf in data. - return !std::isinf(v); + return true; }); dh::XGBCachingDeviceAllocator alloc; // The default implementation in thrust optimizes any_of/none_of/all_of by using small diff --git a/src/data/ellpack_page.cu b/src/data/ellpack_page.cu index 44b9c8dd6..c60fe8389 100644 --- a/src/data/ellpack_page.cu +++ b/src/data/ellpack_page.cu @@ -1,5 +1,5 @@ /** - * Copyright 2019-2023 by XGBoost contributors + * Copyright 2019-2024, XGBoost contributors */ #include #include @@ -13,7 +13,7 @@ #include "../common/hist_util.cuh" #include "../common/transform_iterator.h" // MakeIndexTransformIter #include "./ellpack_page.cuh" -#include "device_adapter.cuh" // for HasInfInData +#include "device_adapter.cuh" // for NoInfInData #include "ellpack_page.h" #include "gradient_index.h" #include "xgboost/data.h" diff --git a/src/data/simple_dmatrix.cuh b/src/data/simple_dmatrix.cuh index 47d736050..528bea8be 100644 --- a/src/data/simple_dmatrix.cuh +++ b/src/data/simple_dmatrix.cuh @@ -1,5 +1,5 @@ /** - * Copyright 2019-2023 by XGBoost Contributors + * Copyright 2019-2024, XGBoost Contributors * \file simple_dmatrix.cuh */ #ifndef XGBOOST_DATA_SIMPLE_DMATRIX_CUH_ @@ -11,7 +11,7 @@ #include "../common/device_helpers.cuh" #include "../common/error_msg.h" // for InfInData -#include "device_adapter.cuh" // for HasInfInData +#include "device_adapter.cuh" // for NoInfInData namespace xgboost::data { diff --git a/tests/buildkite/test-win64-gpu.ps1 b/tests/buildkite/test-win64-gpu.ps1 index fcc6e8436..95a51b502 100644 --- a/tests/buildkite/test-win64-gpu.ps1 +++ b/tests/buildkite/test-win64-gpu.ps1 @@ -32,6 +32,8 @@ Foreach-Object { Write-Host "--- Run Python tests" python -X faulthandler -m pytest -v -s -rxXs --fulltrace tests/python +if ($LASTEXITCODE -ne 0) { throw "Last command failed" } Write-Host "--- Run Python tests with GPU" python -X faulthandler -m pytest -v -s -rxXs --fulltrace -m "(not slow) and (not mgpu)"` tests/python-gpu +if ($LASTEXITCODE -ne 0) { throw "Last command failed" } diff --git a/tests/python-gpu/test_gpu_prediction.py b/tests/python-gpu/test_gpu_prediction.py index ec7c45ca2..a1bc13cb8 100644 --- a/tests/python-gpu/test_gpu_prediction.py +++ b/tests/python-gpu/test_gpu_prediction.py @@ -152,6 +152,7 @@ class TestGPUPredict: @pytest.mark.parametrize("device", ["cpu", "cuda"]) @pytest.mark.skipif(**tm.no_cupy()) + @pytest.mark.skipif(**tm.no_cudf()) def test_inplace_predict_device_type(self, device: str) -> None: """Test inplace predict with different device and data types. diff --git a/tests/python-gpu/test_gpu_with_sklearn.py b/tests/python-gpu/test_gpu_with_sklearn.py index 650c0a047..a01e79ccc 100644 --- a/tests/python-gpu/test_gpu_with_sklearn.py +++ b/tests/python-gpu/test_gpu_with_sklearn.py @@ -249,7 +249,7 @@ def test_custom_objective( clf.fit(X, y) -@pytest.mark.skipif(**tm.no_pandas()) +@pytest.mark.skipif(**tm.no_cudf()) def test_ranking_qid_df(): import cudf From d3f2dbe64ff3cfd1106c57c83bf00ae652a16a3a Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Fri, 26 Jan 2024 02:09:38 +0800 Subject: [PATCH 03/16] [dask] Add seed to demos. (#10009) --- demo/dask/cpu_training.py | 5 +++-- demo/dask/gpu_training.py | 7 +++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/demo/dask/cpu_training.py b/demo/dask/cpu_training.py index 0f3316741..00453740f 100644 --- a/demo/dask/cpu_training.py +++ b/demo/dask/cpu_training.py @@ -14,8 +14,9 @@ def main(client): # generate some random data for demonstration m = 100000 n = 100 - X = da.random.random(size=(m, n), chunks=100) - y = da.random.random(size=(m,), chunks=100) + rng = da.random.default_rng(1) + X = rng.normal(size=(m, n)) + y = X.sum(axis=1) # DaskDMatrix acts like normal DMatrix, works as a proxy for local # DMatrix scatter around workers. diff --git a/demo/dask/gpu_training.py b/demo/dask/gpu_training.py index fd5b35bf3..6096b87fc 100644 --- a/demo/dask/gpu_training.py +++ b/demo/dask/gpu_training.py @@ -2,6 +2,7 @@ Example of training with Dask on GPU ==================================== """ +import cupy as cp import dask_cudf from dask import array as da from dask import dataframe as dd @@ -72,10 +73,12 @@ if __name__ == "__main__": with LocalCUDACluster(n_workers=2, threads_per_worker=4) as cluster: with Client(cluster) as client: # generate some random data for demonstration + rng = da.random.default_rng(1) + m = 100000 n = 100 - X = da.random.random(size=(m, n), chunks=10000) - y = da.random.random(size=(m,), chunks=10000) + X = rng.normal(size=(m, n)) + y = X.sum(axis=1) print("Using DaskQuantileDMatrix") from_ddqdm = using_quantile_device_dmatrix(client, X, y) From a76d6c6131bf7f67b9afe2273b6a0e5673ce2640 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Fri, 26 Jan 2024 02:13:40 +0800 Subject: [PATCH 04/16] Fix cpp deprecation. (#10010) --- src/common/transform_iterator.h | 14 +++++++------- src/data/array_interface.h | 6 +++--- src/data/proxy_dmatrix.h | 9 +++++---- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/common/transform_iterator.h b/src/common/transform_iterator.h index 2efb0b725..8125bd852 100644 --- a/src/common/transform_iterator.h +++ b/src/common/transform_iterator.h @@ -1,12 +1,12 @@ /** - * Copyright 2022 by XGBoost Contributors + * Copyright 2022-2024, XGBoost Contributors */ #ifndef XGBOOST_COMMON_TRANSFORM_ITERATOR_H_ #define XGBOOST_COMMON_TRANSFORM_ITERATOR_H_ #include // std::size_t #include // std::random_access_iterator_tag -#include // std::result_of_t, std::add_pointer_t, std::add_lvalue_reference_t +#include // for invoke_result_t, add_pointer_t, add_lvalue_reference_t #include // std::forward #include "xgboost/span.h" // ptrdiff_t @@ -25,11 +25,11 @@ class IndexTransformIter { Fn fn_; public: - using iterator_category = std::random_access_iterator_tag; // NOLINT - using reference = std::result_of_t; // NOLINT - using value_type = std::remove_cv_t>; // NOLINT - using difference_type = detail::ptrdiff_t; // NOLINT - using pointer = std::add_pointer_t; // NOLINT + using iterator_category = std::random_access_iterator_tag; // NOLINT + using reference = std::invoke_result_t; // NOLINT + using value_type = std::remove_cv_t>; // NOLINT + using difference_type = detail::ptrdiff_t; // NOLINT + using pointer = std::add_pointer_t; // NOLINT public: /** diff --git a/src/data/array_interface.h b/src/data/array_interface.h index 6f2438f37..d645c9e75 100644 --- a/src/data/array_interface.h +++ b/src/data/array_interface.h @@ -1,5 +1,5 @@ /** - * Copyright 2019-2023 by XGBoost Contributors + * Copyright 2019-2024, XGBoost Contributors * \file array_interface.h * \brief View of __array_interface__ */ @@ -12,7 +12,7 @@ #include // for numeric_limits #include #include -#include // std::alignment_of,std::remove_pointer_t +#include // for alignment_of, remove_pointer_t, invoke_result_t #include #include @@ -643,7 +643,7 @@ auto DispatchDType(ArrayInterfaceHandler::Type dtype, Fn dispatch) { } } - return std::result_of_t(); + return std::invoke_result_t(); } template diff --git a/src/data/proxy_dmatrix.h b/src/data/proxy_dmatrix.h index 544c4c81c..7efff7af4 100644 --- a/src/data/proxy_dmatrix.h +++ b/src/data/proxy_dmatrix.h @@ -1,5 +1,5 @@ /** - * Copyright 2020-2023, XGBoost contributors + * Copyright 2020-2024, XGBoost contributors */ #ifndef XGBOOST_DATA_PROXY_DMATRIX_H_ #define XGBOOST_DATA_PROXY_DMATRIX_H_ @@ -7,6 +7,7 @@ #include // for any, any_cast #include #include +#include // for invoke_result_t #include #include "adapter.h" @@ -171,10 +172,10 @@ decltype(auto) HostAdapterDispatch(DMatrixProxy const* proxy, Fn fn, bool* type_ LOG(FATAL) << "Unknown type: " << proxy->Adapter().type().name(); } if constexpr (get_value) { - return std::result_of_t>()->Value()))>(); + return std::invoke_result_t< + Fn, decltype(std::declval>()->Value())>(); } else { - return std::result_of_t>()))>(); + return std::invoke_result_t>())>(); } } } From 65d7bf2dfeb4ab737e4097ab39f887150be04a92 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Fri, 26 Jan 2024 04:58:48 +0800 Subject: [PATCH 05/16] Handle np integer in model slice and prediction. (#10007) --- python-package/xgboost/_typing.py | 5 ++++ python-package/xgboost/core.py | 35 ++++++++++++++--------- python-package/xgboost/dask/__init__.py | 24 ++++++++-------- python-package/xgboost/sklearn.py | 18 ++++++------ tests/python/test_basic_models.py | 38 ++++++++++++++++--------- tests/python/test_predict.py | 2 +- tests/python/test_with_sklearn.py | 2 +- 7 files changed, 75 insertions(+), 49 deletions(-) diff --git a/python-package/xgboost/_typing.py b/python-package/xgboost/_typing.py index a36757a81..da8215c0d 100644 --- a/python-package/xgboost/_typing.py +++ b/python-package/xgboost/_typing.py @@ -36,6 +36,11 @@ PandasDType = Any # real type is pandas.core.dtypes.base.ExtensionDtype FloatCompatible = Union[float, np.float32, np.float64] +# typing.SupportsInt is not suitable here since floating point values are convertible to +# integers as well. +Integer = Union[int, np.integer] +IterationRange = Tuple[Integer, Integer] + # callables FPreProcCallable = Callable diff --git a/python-package/xgboost/core.py b/python-package/xgboost/core.py index 36be766b1..f88472852 100644 --- a/python-package/xgboost/core.py +++ b/python-package/xgboost/core.py @@ -48,6 +48,8 @@ from ._typing import ( FeatureInfo, FeatureNames, FeatureTypes, + Integer, + IterationRange, ModelIn, NumpyOrCupy, TransformedData, @@ -1812,19 +1814,25 @@ class Booster: state["handle"] = handle self.__dict__.update(state) - def __getitem__(self, val: Union[int, tuple, slice]) -> "Booster": + def __getitem__(self, val: Union[Integer, tuple, slice]) -> "Booster": """Get a slice of the tree-based model. .. versionadded:: 1.3.0 """ - if isinstance(val, int): - val = slice(val, val + 1) + # convert to slice for all other types + if isinstance(val, (np.integer, int)): + val = slice(int(val), int(val + 1)) + if isinstance(val, type(Ellipsis)): + val = slice(0, 0) if isinstance(val, tuple): raise ValueError("Only supports slicing through 1 dimension.") + # All supported types are now slice + # FIXME(jiamingy): Use `types.EllipsisType` once Python 3.10 is used. if not isinstance(val, slice): - msg = _expect((int, slice), type(val)) + msg = _expect((int, slice, np.integer, type(Ellipsis)), type(val)) raise TypeError(msg) + if isinstance(val.start, type(Ellipsis)) or val.start is None: start = 0 else: @@ -2246,12 +2254,13 @@ class Booster: pred_interactions: bool = False, validate_features: bool = True, training: bool = False, - iteration_range: Tuple[int, int] = (0, 0), + iteration_range: IterationRange = (0, 0), strict_shape: bool = False, ) -> np.ndarray: - """Predict with data. The full model will be used unless `iteration_range` is specified, - meaning user have to either slice the model or use the ``best_iteration`` - attribute to get prediction from best model returned from early stopping. + """Predict with data. The full model will be used unless `iteration_range` is + specified, meaning user have to either slice the model or use the + ``best_iteration`` attribute to get prediction from best model returned from + early stopping. .. note:: @@ -2336,8 +2345,8 @@ class Booster: args = { "type": 0, "training": training, - "iteration_begin": iteration_range[0], - "iteration_end": iteration_range[1], + "iteration_begin": int(iteration_range[0]), + "iteration_end": int(iteration_range[1]), "strict_shape": strict_shape, } @@ -2373,7 +2382,7 @@ class Booster: def inplace_predict( self, data: DataType, - iteration_range: Tuple[int, int] = (0, 0), + iteration_range: IterationRange = (0, 0), predict_type: str = "value", missing: float = np.nan, validate_features: bool = True, @@ -2439,8 +2448,8 @@ class Booster: args = make_jcargs( type=1 if predict_type == "margin" else 0, training=False, - iteration_begin=iteration_range[0], - iteration_end=iteration_range[1], + iteration_begin=int(iteration_range[0]), + iteration_end=int(iteration_range[1]), missing=missing, strict_shape=strict_shape, cache_id=0, diff --git a/python-package/xgboost/dask/__init__.py b/python-package/xgboost/dask/__init__.py index a9b51f35d..c80bd932a 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 +from xgboost._typing import _T, FeatureNames, FeatureTypes, IterationRange from xgboost.callback import TrainingCallback from xgboost.compat import DataFrame, LazyLoader, concat, lazy_isinstance from xgboost.core import ( @@ -1263,7 +1263,7 @@ async def _predict_async( approx_contribs: bool, pred_interactions: bool, validate_features: bool, - iteration_range: Tuple[int, int], + iteration_range: IterationRange, strict_shape: bool, ) -> _DaskCollection: _booster = await _get_model_future(client, model) @@ -1410,7 +1410,7 @@ def predict( # pylint: disable=unused-argument approx_contribs: bool = False, pred_interactions: bool = False, validate_features: bool = True, - iteration_range: Tuple[int, int] = (0, 0), + iteration_range: IterationRange = (0, 0), strict_shape: bool = False, ) -> Any: """Run prediction with a trained booster. @@ -1458,7 +1458,7 @@ async def _inplace_predict_async( # pylint: disable=too-many-branches global_config: Dict[str, Any], model: Union[Booster, Dict, "distributed.Future"], data: _DataT, - iteration_range: Tuple[int, int], + iteration_range: IterationRange, predict_type: str, missing: float, validate_features: bool, @@ -1516,7 +1516,7 @@ def inplace_predict( # pylint: disable=unused-argument client: Optional["distributed.Client"], model: Union[TrainReturnT, Booster, "distributed.Future"], data: _DataT, - iteration_range: Tuple[int, int] = (0, 0), + iteration_range: IterationRange = (0, 0), predict_type: str = "value", missing: float = numpy.nan, validate_features: bool = True, @@ -1624,7 +1624,7 @@ class DaskScikitLearnBase(XGBModel): output_margin: bool, validate_features: bool, base_margin: Optional[_DaskCollection], - iteration_range: Optional[Tuple[int, int]], + iteration_range: Optional[IterationRange], ) -> Any: iteration_range = self._get_iteration_range(iteration_range) if self._can_use_inplace_predict(): @@ -1664,7 +1664,7 @@ class DaskScikitLearnBase(XGBModel): output_margin: bool = False, validate_features: bool = True, base_margin: Optional[_DaskCollection] = None, - iteration_range: Optional[Tuple[int, int]] = None, + iteration_range: Optional[IterationRange] = None, ) -> Any: _assert_dask_support() return self.client.sync( @@ -1679,7 +1679,7 @@ class DaskScikitLearnBase(XGBModel): async def _apply_async( self, X: _DataT, - iteration_range: Optional[Tuple[int, int]] = None, + iteration_range: Optional[IterationRange] = None, ) -> Any: iteration_range = self._get_iteration_range(iteration_range) test_dmatrix = await DaskDMatrix( @@ -1700,7 +1700,7 @@ class DaskScikitLearnBase(XGBModel): def apply( self, X: _DataT, - iteration_range: Optional[Tuple[int, int]] = None, + iteration_range: Optional[IterationRange] = None, ) -> Any: _assert_dask_support() return self.client.sync(self._apply_async, X, iteration_range=iteration_range) @@ -1962,7 +1962,7 @@ class DaskXGBClassifier(DaskScikitLearnBase, XGBClassifierBase): X: _DataT, validate_features: bool, base_margin: Optional[_DaskCollection], - iteration_range: Optional[Tuple[int, int]], + iteration_range: Optional[IterationRange], ) -> _DaskCollection: if self.objective == "multi:softmax": raise ValueError( @@ -1987,7 +1987,7 @@ class DaskXGBClassifier(DaskScikitLearnBase, XGBClassifierBase): X: _DaskCollection, validate_features: bool = True, base_margin: Optional[_DaskCollection] = None, - iteration_range: Optional[Tuple[int, int]] = None, + iteration_range: Optional[IterationRange] = None, ) -> Any: _assert_dask_support() return self._client_sync( @@ -2006,7 +2006,7 @@ class DaskXGBClassifier(DaskScikitLearnBase, XGBClassifierBase): output_margin: bool, validate_features: bool, base_margin: Optional[_DaskCollection], - iteration_range: Optional[Tuple[int, int]], + iteration_range: Optional[IterationRange], ) -> _DaskCollection: pred_probs = await super()._predict_async( data, output_margin, validate_features, base_margin, iteration_range diff --git a/python-package/xgboost/sklearn.py b/python-package/xgboost/sklearn.py index a0fde2292..5d651948c 100644 --- a/python-package/xgboost/sklearn.py +++ b/python-package/xgboost/sklearn.py @@ -22,7 +22,7 @@ from typing import ( import numpy as np from scipy.special import softmax -from ._typing import ArrayLike, FeatureNames, FeatureTypes, ModelIn +from ._typing import ArrayLike, FeatureNames, FeatureTypes, IterationRange, ModelIn from .callback import TrainingCallback # Do not use class names on scikit-learn directly. Re-define the classes on @@ -1039,8 +1039,8 @@ class XGBModel(XGBModelBase): return False def _get_iteration_range( - self, iteration_range: Optional[Tuple[int, int]] - ) -> Tuple[int, int]: + self, iteration_range: Optional[IterationRange] + ) -> IterationRange: if iteration_range is None or iteration_range[1] == 0: # Use best_iteration if defined. try: @@ -1057,7 +1057,7 @@ class XGBModel(XGBModelBase): output_margin: bool = False, validate_features: bool = True, base_margin: Optional[ArrayLike] = None, - iteration_range: Optional[Tuple[int, int]] = None, + iteration_range: Optional[IterationRange] = None, ) -> ArrayLike: """Predict with `X`. If the model is trained with early stopping, then :py:attr:`best_iteration` is used automatically. The estimator uses @@ -1129,7 +1129,7 @@ class XGBModel(XGBModelBase): def apply( self, X: ArrayLike, - iteration_range: Optional[Tuple[int, int]] = None, + iteration_range: Optional[IterationRange] = None, ) -> np.ndarray: """Return the predicted leaf every tree for each sample. If the model is trained with early stopping, then :py:attr:`best_iteration` is used automatically. @@ -1465,7 +1465,7 @@ class XGBClassifier(XGBModel, XGBClassifierBase): output_margin: bool = False, validate_features: bool = True, base_margin: Optional[ArrayLike] = None, - iteration_range: Optional[Tuple[int, int]] = None, + iteration_range: Optional[IterationRange] = None, ) -> ArrayLike: with config_context(verbosity=self.verbosity): class_probs = super().predict( @@ -1500,7 +1500,7 @@ class XGBClassifier(XGBModel, XGBClassifierBase): X: ArrayLike, validate_features: bool = True, base_margin: Optional[ArrayLike] = None, - iteration_range: Optional[Tuple[int, int]] = None, + iteration_range: Optional[IterationRange] = None, ) -> np.ndarray: """Predict the probability of each `X` example being of a given class. If the model is trained with early stopping, then :py:attr:`best_iteration` is used @@ -1942,7 +1942,7 @@ class XGBRanker(XGBModel, XGBRankerMixIn): output_margin: bool = False, validate_features: bool = True, base_margin: Optional[ArrayLike] = None, - iteration_range: Optional[Tuple[int, int]] = None, + iteration_range: Optional[IterationRange] = None, ) -> ArrayLike: X, _ = _get_qid(X, None) return super().predict( @@ -1956,7 +1956,7 @@ class XGBRanker(XGBModel, XGBRankerMixIn): def apply( self, X: ArrayLike, - iteration_range: Optional[Tuple[int, int]] = None, + iteration_range: Optional[IterationRange] = None, ) -> ArrayLike: X, _ = _get_qid(X, None) return super().apply(X, iteration_range) diff --git a/tests/python/test_basic_models.py b/tests/python/test_basic_models.py index 828c24862..07bf82a59 100644 --- a/tests/python/test_basic_models.py +++ b/tests/python/test_basic_models.py @@ -7,6 +7,7 @@ import pytest import xgboost as xgb from xgboost import testing as tm +from xgboost.core import Integer from xgboost.testing.updater import ResetStrategy dpath = tm.data_dir(__file__) @@ -97,15 +98,15 @@ class TestModels: def test_boost_from_prediction(self): # Re-construct dtrain here to avoid modification margined, _ = tm.load_agaricus(__file__) - bst = xgb.train({'tree_method': 'hist'}, margined, 1) + bst = xgb.train({"tree_method": "hist"}, margined, 1) predt_0 = bst.predict(margined, output_margin=True) margined.set_base_margin(predt_0) - bst = xgb.train({'tree_method': 'hist'}, margined, 1) + bst = xgb.train({"tree_method": "hist"}, margined, 1) predt_1 = bst.predict(margined) assert np.any(np.abs(predt_1 - predt_0) > 1e-6) dtrain, _ = tm.load_agaricus(__file__) - bst = xgb.train({'tree_method': 'hist'}, dtrain, 2) + bst = xgb.train({"tree_method": "hist"}, dtrain, 2) predt_2 = bst.predict(dtrain) assert np.all(np.abs(predt_2 - predt_1) < 1e-6) @@ -331,10 +332,15 @@ class TestModels: dtrain: xgb.DMatrix, num_parallel_tree: int, num_classes: int, - num_boost_round: int + num_boost_round: int, + use_np_type: bool, ): beg = 3 - end = 7 + if use_np_type: + end: Integer = np.int32(7) + else: + end = 7 + sliced: xgb.Booster = booster[beg:end] assert sliced.feature_types == booster.feature_types @@ -345,7 +351,7 @@ class TestModels: sliced = booster[beg:end:2] assert sliced_trees == len(sliced.get_dump()) - sliced = booster[beg: ...] + sliced = booster[beg:] sliced_trees = (num_boost_round - beg) * num_parallel_tree * num_classes assert sliced_trees == len(sliced.get_dump()) @@ -357,7 +363,7 @@ class TestModels: sliced_trees = end * num_parallel_tree * num_classes assert sliced_trees == len(sliced.get_dump()) - sliced = booster[...: end] + sliced = booster[: end] sliced_trees = end * num_parallel_tree * num_classes assert sliced_trees == len(sliced.get_dump()) @@ -383,14 +389,14 @@ class TestModels: assert len(trees) == num_boost_round with pytest.raises(TypeError): - booster["wrong type"] + booster["wrong type"] # type: ignore with pytest.raises(IndexError): booster[: num_boost_round + 1] with pytest.raises(ValueError): booster[1, 2] # too many dims # setitem is not implemented as model is immutable during slicing. with pytest.raises(TypeError): - booster[...: end] = booster + booster[:end] = booster # type: ignore sliced_0 = booster[1:3] np.testing.assert_allclose( @@ -446,15 +452,21 @@ class TestModels: assert len(booster.get_dump()) == total_trees - self.run_slice(booster, dtrain, num_parallel_tree, num_classes, num_boost_round) + self.run_slice( + booster, dtrain, num_parallel_tree, num_classes, num_boost_round, False + ) bytesarray = booster.save_raw(raw_format="ubj") booster = xgb.Booster(model_file=bytesarray) - self.run_slice(booster, dtrain, num_parallel_tree, num_classes, num_boost_round) + self.run_slice( + booster, dtrain, num_parallel_tree, num_classes, num_boost_round, False + ) bytesarray = booster.save_raw(raw_format="deprecated") booster = xgb.Booster(model_file=bytesarray) - self.run_slice(booster, dtrain, num_parallel_tree, num_classes, num_boost_round) + self.run_slice( + booster, dtrain, num_parallel_tree, num_classes, num_boost_round, True + ) def test_slice_multi(self) -> None: from sklearn.datasets import make_classification @@ -479,7 +491,7 @@ class TestModels: }, num_boost_round=num_boost_round, dtrain=Xy, - callbacks=[ResetStrategy()] + callbacks=[ResetStrategy()], ) sliced = [t for t in booster] assert len(sliced) == 16 diff --git a/tests/python/test_predict.py b/tests/python/test_predict.py index 6ed9c39f7..abea3dd79 100644 --- a/tests/python/test_predict.py +++ b/tests/python/test_predict.py @@ -61,7 +61,7 @@ def run_predict_leaf(device: str) -> np.ndarray: validate_leaf_output(leaf, num_parallel_tree) - n_iters = 2 + n_iters = np.int32(2) sliced = booster.predict( m, pred_leaf=True, diff --git a/tests/python/test_with_sklearn.py b/tests/python/test_with_sklearn.py index 9047cee6e..344628e4f 100644 --- a/tests/python/test_with_sklearn.py +++ b/tests/python/test_with_sklearn.py @@ -440,7 +440,7 @@ def test_regression(): preds = xgb_model.predict(X[test_index]) # test other params in XGBRegressor().fit preds2 = xgb_model.predict( - X[test_index], output_margin=True, iteration_range=(0, 3) + X[test_index], output_margin=True, iteration_range=(0, np.int16(3)) ) preds3 = xgb_model.predict( X[test_index], output_margin=True, iteration_range=None From 54b71c8fba84fd6df645f0b85ab6b25faf97a319 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Tue, 30 Jan 2024 17:24:11 +0800 Subject: [PATCH 06/16] Fix with black 24.1.1. (#10014) --- demo/aft_survival/aft_survival_viz_demo.py | 1 + demo/dask/cpu_training.py | 1 + demo/dask/dask_callbacks.py | 1 + demo/dask/gpu_training.py | 1 + demo/dask/sklearn_cpu_training.py | 1 + demo/guide-python/callbacks.py | 1 + demo/guide-python/cat_pipeline.py | 1 + demo/guide-python/categorical.py | 1 + demo/guide-python/external_memory.py | 1 + demo/guide-python/individual_trees.py | 1 + demo/guide-python/learning_to_rank.py | 1 + demo/guide-python/quantile_regression.py | 1 + demo/guide-python/sklearn_examples.py | 1 + demo/guide-python/sklearn_parallel.py | 1 + demo/guide-python/spark_estimator_examples.py | 1 + demo/rmm_plugin/rmm_mgpu_with_dask.py | 1 + demo/rmm_plugin/rmm_singlegpu.py | 1 + python-package/hatch_build.py | 1 + python-package/packager/build_config.py | 1 + python-package/packager/nativelib.py | 1 + python-package/packager/pep517.py | 1 + python-package/packager/sdist.py | 1 + python-package/packager/util.py | 1 + python-package/xgboost/collective.py | 1 + python-package/xgboost/core.py | 6 ++---- python-package/xgboost/dask/__init__.py | 6 +++--- python-package/xgboost/dask/utils.py | 1 + python-package/xgboost/spark/core.py | 1 + python-package/xgboost/spark/estimator.py | 1 + python-package/xgboost/spark/params.py | 2 +- python-package/xgboost/spark/utils.py | 1 + python-package/xgboost/testing/__init__.py | 1 + python-package/xgboost/testing/continuation.py | 1 + python-package/xgboost/testing/dask.py | 1 + python-package/xgboost/testing/data_iter.py | 1 + python-package/xgboost/testing/metrics.py | 1 + python-package/xgboost/testing/shared.py | 1 + python-package/xgboost/testing/updater.py | 1 + tests/ci_build/change_version.py | 1 + tests/ci_build/test_r_package.py | 1 + tests/ci_build/test_utils.py | 1 + tests/python-gpu/load_pickle.py | 1 + tests/python-gpu/test_gpu_pickling.py | 1 + tests/python/test_predict.py | 1 + .../test_gpu_with_dask/test_gpu_with_dask.py | 1 + tests/test_distributed/test_with_dask/test_with_dask.py | 1 + 46 files changed, 49 insertions(+), 8 deletions(-) diff --git a/demo/aft_survival/aft_survival_viz_demo.py b/demo/aft_survival/aft_survival_viz_demo.py index b925ca547..0e434a151 100644 --- a/demo/aft_survival/aft_survival_viz_demo.py +++ b/demo/aft_survival/aft_survival_viz_demo.py @@ -6,6 +6,7 @@ This demo uses 1D toy data and visualizes how XGBoost fits a tree ensemble. The model starts out as a flat line and evolves into a step function in order to account for all ranged labels. """ + import matplotlib.pyplot as plt import numpy as np diff --git a/demo/dask/cpu_training.py b/demo/dask/cpu_training.py index 00453740f..2bee444f7 100644 --- a/demo/dask/cpu_training.py +++ b/demo/dask/cpu_training.py @@ -3,6 +3,7 @@ Example of training with Dask on CPU ==================================== """ + from dask import array as da from dask.distributed import Client, LocalCluster diff --git a/demo/dask/dask_callbacks.py b/demo/dask/dask_callbacks.py index a4b0f5648..4a7ec0f19 100644 --- a/demo/dask/dask_callbacks.py +++ b/demo/dask/dask_callbacks.py @@ -2,6 +2,7 @@ Example of using callbacks with Dask ==================================== """ + import numpy as np from dask.distributed import Client, LocalCluster from dask_ml.datasets import make_regression diff --git a/demo/dask/gpu_training.py b/demo/dask/gpu_training.py index 6096b87fc..f53835ffb 100644 --- a/demo/dask/gpu_training.py +++ b/demo/dask/gpu_training.py @@ -2,6 +2,7 @@ Example of training with Dask on GPU ==================================== """ + import cupy as cp import dask_cudf from dask import array as da diff --git a/demo/dask/sklearn_cpu_training.py b/demo/dask/sklearn_cpu_training.py index 38ea25e61..e91babb84 100644 --- a/demo/dask/sklearn_cpu_training.py +++ b/demo/dask/sklearn_cpu_training.py @@ -2,6 +2,7 @@ Use scikit-learn regressor interface with CPU histogram tree method =================================================================== """ + from dask import array as da from dask.distributed import Client, LocalCluster diff --git a/demo/guide-python/callbacks.py b/demo/guide-python/callbacks.py index 0676c732e..2f8ac5c79 100644 --- a/demo/guide-python/callbacks.py +++ b/demo/guide-python/callbacks.py @@ -4,6 +4,7 @@ Demo for using and defining callback functions .. versionadded:: 1.3.0 """ + import argparse import os import tempfile diff --git a/demo/guide-python/cat_pipeline.py b/demo/guide-python/cat_pipeline.py index 0f2ba8f8d..72e786edd 100644 --- a/demo/guide-python/cat_pipeline.py +++ b/demo/guide-python/cat_pipeline.py @@ -13,6 +13,7 @@ See Also - :ref:`sphx_glr_python_examples_cat_in_the_dat.py` """ + from typing import List, Tuple import numpy as np diff --git a/demo/guide-python/categorical.py b/demo/guide-python/categorical.py index b639f4359..d42fb8b77 100644 --- a/demo/guide-python/categorical.py +++ b/demo/guide-python/categorical.py @@ -17,6 +17,7 @@ See Also - :ref:`sphx_glr_python_examples_cat_pipeline.py` """ + from typing import Tuple import numpy as np diff --git a/demo/guide-python/external_memory.py b/demo/guide-python/external_memory.py index 6d789486e..e4d1895d1 100644 --- a/demo/guide-python/external_memory.py +++ b/demo/guide-python/external_memory.py @@ -11,6 +11,7 @@ instead of Quantile DMatrix. The feature is not ready for production use yet. See :doc:`the tutorial ` for more details. """ + import os import tempfile from typing import Callable, List, Tuple diff --git a/demo/guide-python/individual_trees.py b/demo/guide-python/individual_trees.py index 93a9aad2b..b10fabf64 100644 --- a/demo/guide-python/individual_trees.py +++ b/demo/guide-python/individual_trees.py @@ -2,6 +2,7 @@ Demo for prediction using individual trees and model slices =========================================================== """ + import os import numpy as np diff --git a/demo/guide-python/learning_to_rank.py b/demo/guide-python/learning_to_rank.py index 62df8253b..b131b31f7 100644 --- a/demo/guide-python/learning_to_rank.py +++ b/demo/guide-python/learning_to_rank.py @@ -15,6 +15,7 @@ position debiasing training. For an overview of learning to rank in XGBoost, please see :doc:`Learning to Rank `. """ + from __future__ import annotations import argparse diff --git a/demo/guide-python/quantile_regression.py b/demo/guide-python/quantile_regression.py index a9e4532ba..f44b5c9b4 100644 --- a/demo/guide-python/quantile_regression.py +++ b/demo/guide-python/quantile_regression.py @@ -13,6 +13,7 @@ https://scikit-learn.org/stable/auto_examples/ensemble/plot_gradient_boosting_qu crossing can happen due to limitation in the algorithm. """ + import argparse from typing import Dict diff --git a/demo/guide-python/sklearn_examples.py b/demo/guide-python/sklearn_examples.py index 0fe7a8e24..42baf6883 100644 --- a/demo/guide-python/sklearn_examples.py +++ b/demo/guide-python/sklearn_examples.py @@ -9,6 +9,7 @@ Created on 1 Apr 2015 @author: Jamie Hall """ + import pickle import numpy as np diff --git a/demo/guide-python/sklearn_parallel.py b/demo/guide-python/sklearn_parallel.py index 55e0bff74..db5303ab7 100644 --- a/demo/guide-python/sklearn_parallel.py +++ b/demo/guide-python/sklearn_parallel.py @@ -2,6 +2,7 @@ Demo for using xgboost with sklearn =================================== """ + import multiprocessing from sklearn.datasets import fetch_california_housing diff --git a/demo/guide-python/spark_estimator_examples.py b/demo/guide-python/spark_estimator_examples.py index 97caef610..ac36065bc 100644 --- a/demo/guide-python/spark_estimator_examples.py +++ b/demo/guide-python/spark_estimator_examples.py @@ -4,6 +4,7 @@ Collection of examples for using xgboost.spark estimator interface @author: Weichen Xu """ + import sklearn.datasets from pyspark.ml.evaluation import MulticlassClassificationEvaluator, RegressionEvaluator from pyspark.ml.linalg import Vectors diff --git a/demo/rmm_plugin/rmm_mgpu_with_dask.py b/demo/rmm_plugin/rmm_mgpu_with_dask.py index 2384b209e..467827074 100644 --- a/demo/rmm_plugin/rmm_mgpu_with_dask.py +++ b/demo/rmm_plugin/rmm_mgpu_with_dask.py @@ -2,6 +2,7 @@ Using rmm with Dask =================== """ + import dask from dask.distributed import Client from dask_cuda import LocalCUDACluster diff --git a/demo/rmm_plugin/rmm_singlegpu.py b/demo/rmm_plugin/rmm_singlegpu.py index b4dccd805..a1457406b 100644 --- a/demo/rmm_plugin/rmm_singlegpu.py +++ b/demo/rmm_plugin/rmm_singlegpu.py @@ -2,6 +2,7 @@ Using rmm on a single node device ================================= """ + import rmm from sklearn.datasets import make_classification diff --git a/python-package/hatch_build.py b/python-package/hatch_build.py index 696787fa2..925c917b9 100644 --- a/python-package/hatch_build.py +++ b/python-package/hatch_build.py @@ -2,6 +2,7 @@ Custom hook to customize the behavior of Hatchling. Here, we customize the tag of the generated wheels. """ + import sysconfig from typing import Any, Dict diff --git a/python-package/packager/build_config.py b/python-package/packager/build_config.py index 933bfdce2..8a4347a16 100644 --- a/python-package/packager/build_config.py +++ b/python-package/packager/build_config.py @@ -1,4 +1,5 @@ """Build configuration""" + import dataclasses from typing import Any, Dict, List, Optional diff --git a/python-package/packager/nativelib.py b/python-package/packager/nativelib.py index 9d3fec2bc..0227cff37 100644 --- a/python-package/packager/nativelib.py +++ b/python-package/packager/nativelib.py @@ -1,6 +1,7 @@ """ Functions for building libxgboost """ + import logging import os import pathlib diff --git a/python-package/packager/pep517.py b/python-package/packager/pep517.py index 2c4f9e3e6..d2e671fb6 100644 --- a/python-package/packager/pep517.py +++ b/python-package/packager/pep517.py @@ -4,6 +4,7 @@ Builds source distribution and binary wheels, following PEP 517 / PEP 660. Reuses components of Hatchling (https://github.com/pypa/hatch/tree/master/backend) for the sake of brevity. """ + import dataclasses import logging import os diff --git a/python-package/packager/sdist.py b/python-package/packager/sdist.py index af9fbca0d..4c70c24fe 100644 --- a/python-package/packager/sdist.py +++ b/python-package/packager/sdist.py @@ -1,6 +1,7 @@ """ Functions for building sdist """ + import logging import pathlib diff --git a/python-package/packager/util.py b/python-package/packager/util.py index 0fff062d7..866f186b3 100644 --- a/python-package/packager/util.py +++ b/python-package/packager/util.py @@ -1,6 +1,7 @@ """ Utility functions for implementing PEP 517 backend """ + import logging import pathlib import shutil diff --git a/python-package/xgboost/collective.py b/python-package/xgboost/collective.py index 4eb5ea2ab..a41d296bf 100644 --- a/python-package/xgboost/collective.py +++ b/python-package/xgboost/collective.py @@ -1,4 +1,5 @@ """XGBoost collective communication related API.""" + import ctypes import json import logging diff --git a/python-package/xgboost/core.py b/python-package/xgboost/core.py index f88472852..5761b4b14 100644 --- a/python-package/xgboost/core.py +++ b/python-package/xgboost/core.py @@ -64,13 +64,11 @@ class XGBoostError(ValueError): @overload -def from_pystr_to_cstr(data: str) -> bytes: - ... +def from_pystr_to_cstr(data: str) -> bytes: ... @overload -def from_pystr_to_cstr(data: List[str]) -> ctypes.Array: - ... +def from_pystr_to_cstr(data: List[str]) -> ctypes.Array: ... def from_pystr_to_cstr(data: Union[str, List[str]]) -> Union[bytes, ctypes.Array]: diff --git a/python-package/xgboost/dask/__init__.py b/python-package/xgboost/dask/__init__.py index c80bd932a..a3c549a2e 100644 --- a/python-package/xgboost/dask/__init__.py +++ b/python-package/xgboost/dask/__init__.py @@ -1146,9 +1146,9 @@ async def _direct_predict_impl( # pylint: disable=too-many-branches if _can_output_df(isinstance(data, dd.DataFrame), output_shape): if base_margin is not None and isinstance(base_margin, da.Array): # Easier for map_partitions - base_margin_df: Optional[ - Union[dd.DataFrame, dd.Series] - ] = base_margin.to_dask_dataframe() + base_margin_df: Optional[Union[dd.DataFrame, dd.Series]] = ( + base_margin.to_dask_dataframe() + ) else: base_margin_df = base_margin predictions = dd.map_partitions( diff --git a/python-package/xgboost/dask/utils.py b/python-package/xgboost/dask/utils.py index 98e6029b5..d433c8072 100644 --- a/python-package/xgboost/dask/utils.py +++ b/python-package/xgboost/dask/utils.py @@ -1,4 +1,5 @@ """Utilities for the XGBoost Dask interface.""" + import logging from typing import TYPE_CHECKING, Any, Dict diff --git a/python-package/xgboost/spark/core.py b/python-package/xgboost/spark/core.py index 7ac01ff07..eb226611d 100644 --- a/python-package/xgboost/spark/core.py +++ b/python-package/xgboost/spark/core.py @@ -1,4 +1,5 @@ """XGBoost pyspark integration submodule for core code.""" + import base64 # pylint: disable=fixme, too-many-ancestors, protected-access, no-member, invalid-name diff --git a/python-package/xgboost/spark/estimator.py b/python-package/xgboost/spark/estimator.py index 193ca4b2a..51e2e946f 100644 --- a/python-package/xgboost/spark/estimator.py +++ b/python-package/xgboost/spark/estimator.py @@ -1,4 +1,5 @@ """Xgboost pyspark integration submodule for estimator API.""" + # pylint: disable=too-many-ancestors # pylint: disable=fixme, too-many-ancestors, protected-access, no-member, invalid-name # pylint: disable=unused-argument, too-many-locals diff --git a/python-package/xgboost/spark/params.py b/python-package/xgboost/spark/params.py index a81f6cd33..a177c73fe 100644 --- a/python-package/xgboost/spark/params.py +++ b/python-package/xgboost/spark/params.py @@ -1,4 +1,5 @@ """Xgboost pyspark integration submodule for params.""" + from typing import Dict # pylint: disable=too-few-public-methods @@ -55,7 +56,6 @@ class HasFeaturesCols(Params): class HasEnableSparseDataOptim(Params): - """ This is a Params based class that is extended by _SparkXGBParams and holds the variable to store the boolean config of enabling sparse data optimization. diff --git a/python-package/xgboost/spark/utils.py b/python-package/xgboost/spark/utils.py index 805aa5c10..84333df53 100644 --- a/python-package/xgboost/spark/utils.py +++ b/python-package/xgboost/spark/utils.py @@ -1,4 +1,5 @@ """Xgboost pyspark integration submodule for helper functions.""" + # pylint: disable=fixme import inspect diff --git a/python-package/xgboost/testing/__init__.py b/python-package/xgboost/testing/__init__.py index 46bbf8800..389066f0e 100644 --- a/python-package/xgboost/testing/__init__.py +++ b/python-package/xgboost/testing/__init__.py @@ -2,6 +2,7 @@ change without notice. """ + # pylint: disable=invalid-name,missing-function-docstring,import-error import gc import importlib.util diff --git a/python-package/xgboost/testing/continuation.py b/python-package/xgboost/testing/continuation.py index 9d6dc0338..16037e37b 100644 --- a/python-package/xgboost/testing/continuation.py +++ b/python-package/xgboost/testing/continuation.py @@ -1,4 +1,5 @@ """Tests for training continuation.""" + import json from typing import Any, Dict, TypeVar diff --git a/python-package/xgboost/testing/dask.py b/python-package/xgboost/testing/dask.py index a939170b4..f46803b29 100644 --- a/python-package/xgboost/testing/dask.py +++ b/python-package/xgboost/testing/dask.py @@ -1,4 +1,5 @@ """Tests for dask shared by different test modules.""" + import numpy as np import pandas as pd from dask import array as da diff --git a/python-package/xgboost/testing/data_iter.py b/python-package/xgboost/testing/data_iter.py index 18f8eb378..42a9dfca0 100644 --- a/python-package/xgboost/testing/data_iter.py +++ b/python-package/xgboost/testing/data_iter.py @@ -1,4 +1,5 @@ """Tests related to the `DataIter` interface.""" + import numpy as np import xgboost diff --git a/python-package/xgboost/testing/metrics.py b/python-package/xgboost/testing/metrics.py index c9f449f22..515c9872c 100644 --- a/python-package/xgboost/testing/metrics.py +++ b/python-package/xgboost/testing/metrics.py @@ -1,4 +1,5 @@ """Tests for evaluation metrics.""" + from typing import Dict, List import numpy as np diff --git a/python-package/xgboost/testing/shared.py b/python-package/xgboost/testing/shared.py index 930873163..0455b77d0 100644 --- a/python-package/xgboost/testing/shared.py +++ b/python-package/xgboost/testing/shared.py @@ -1,4 +1,5 @@ """Testing code shared by other tests.""" + # pylint: disable=invalid-name import collections import importlib.util diff --git a/python-package/xgboost/testing/updater.py b/python-package/xgboost/testing/updater.py index 00c982bd0..c0c014167 100644 --- a/python-package/xgboost/testing/updater.py +++ b/python-package/xgboost/testing/updater.py @@ -1,4 +1,5 @@ """Tests for updaters.""" + import json from functools import partial, update_wrapper from typing import Any, Dict, List diff --git a/tests/ci_build/change_version.py b/tests/ci_build/change_version.py index d1ef8ce09..0bda645d5 100644 --- a/tests/ci_build/change_version.py +++ b/tests/ci_build/change_version.py @@ -7,6 +7,7 @@ needed, run CMake . If this is a RC release, the version for JVM packages has the form ..-RC1 """ + import argparse import datetime import os diff --git a/tests/ci_build/test_r_package.py b/tests/ci_build/test_r_package.py index cc1225b03..dd73f850b 100644 --- a/tests/ci_build/test_r_package.py +++ b/tests/ci_build/test_r_package.py @@ -1,4 +1,5 @@ """Utilities for packaging R code and running tests.""" + import argparse import os import shutil diff --git a/tests/ci_build/test_utils.py b/tests/ci_build/test_utils.py index d4fa02629..adcd05d5a 100644 --- a/tests/ci_build/test_utils.py +++ b/tests/ci_build/test_utils.py @@ -1,4 +1,5 @@ """Utilities for the CI.""" + import os from datetime import datetime, timedelta from functools import wraps diff --git a/tests/python-gpu/load_pickle.py b/tests/python-gpu/load_pickle.py index 2f582e535..402470d99 100644 --- a/tests/python-gpu/load_pickle.py +++ b/tests/python-gpu/load_pickle.py @@ -1,5 +1,6 @@ """Loading a pickled model generated by test_pickling.py, only used by `test_gpu_with_dask.py`""" + import json import os diff --git a/tests/python-gpu/test_gpu_pickling.py b/tests/python-gpu/test_gpu_pickling.py index 10c4c7e45..66c86f170 100644 --- a/tests/python-gpu/test_gpu_pickling.py +++ b/tests/python-gpu/test_gpu_pickling.py @@ -1,4 +1,5 @@ """Test model IO with pickle.""" + import os import pickle import subprocess diff --git a/tests/python/test_predict.py b/tests/python/test_predict.py index abea3dd79..6b59ef540 100644 --- a/tests/python/test_predict.py +++ b/tests/python/test_predict.py @@ -1,4 +1,5 @@ """Tests for running inplace prediction.""" + from concurrent.futures import ThreadPoolExecutor import numpy as np 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 865623671..0d9bc2e6b 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,5 @@ """Copyright 2019-2023, XGBoost contributors""" + import asyncio import json from collections import OrderedDict 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 150e698d3..fdf0d64c4 100644 --- a/tests/test_distributed/test_with_dask/test_with_dask.py +++ b/tests/test_distributed/test_with_dask/test_with_dask.py @@ -1,4 +1,5 @@ """Copyright 2019-2022 XGBoost contributors""" + import asyncio import json import os From d9f4ab557a5778c6b89e0b2eea1a324da8f62c32 Mon Sep 17 00:00:00 2001 From: UncleLLD Date: Tue, 30 Jan 2024 17:24:43 +0800 Subject: [PATCH 07/16] [doc] Fix data format (#10013) --- doc/tutorials/dart.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/tutorials/dart.rst b/doc/tutorials/dart.rst index 0cd9109f4..af6384eba 100644 --- a/doc/tutorials/dart.rst +++ b/doc/tutorials/dart.rst @@ -96,8 +96,8 @@ Sample Script import xgboost as xgb # read in data - dtrain = xgb.DMatrix('demo/data/agaricus.txt.train') - dtest = xgb.DMatrix('demo/data/agaricus.txt.test') + dtrain = xgb.DMatrix('demo/data/agaricus.txt.train?format=libsvm') + dtest = xgb.DMatrix('demo/data/agaricus.txt.test?format=libsvm') # specify parameters via map param = {'booster': 'dart', 'max_depth': 5, 'learning_rate': 0.1, From 3abbbe41accc2ad0df202d40e6b1f79cb1b98a0f Mon Sep 17 00:00:00 2001 From: david-cortes Date: Tue, 30 Jan 2024 12:26:44 +0100 Subject: [PATCH 08/16] [R] Add data iterator, quantile dmatrix, external memory, and missing `feature_types` (#9913) --- R-package/DESCRIPTION | 2 +- R-package/NAMESPACE | 5 + R-package/R/xgb.DMatrix.R | 714 ++++++++++++++++-- R-package/man/xgb.DMatrix.Rd | 108 ++- R-package/man/xgb.DataIter.Rd | 51 ++ R-package/man/xgb.ExternalDMatrix.Rd | 122 +++ R-package/man/xgb.ProxyDMatrix.Rd | 121 +++ .../man/xgb.QuantileDMatrix.from_iterator.Rd | 65 ++ R-package/src/init.c | 16 + R-package/src/xgboost_R.cc | 299 ++++++-- R-package/src/xgboost_R.h | 78 ++ R-package/tests/testthat/test_dmatrix.R | 257 ++++++- python-package/xgboost/core.py | 20 +- 13 files changed, 1754 insertions(+), 104 deletions(-) create mode 100644 R-package/man/xgb.DataIter.Rd create mode 100644 R-package/man/xgb.ExternalDMatrix.Rd create mode 100644 R-package/man/xgb.ProxyDMatrix.Rd create mode 100644 R-package/man/xgb.QuantileDMatrix.from_iterator.Rd diff --git a/R-package/DESCRIPTION b/R-package/DESCRIPTION index bbaf3e75d..66e2b5692 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.3.0 +RoxygenNote: 7.3.1 Encoding: UTF-8 SystemRequirements: GNU make, C++17 diff --git a/R-package/NAMESPACE b/R-package/NAMESPACE index 398b0da5a..49f93bb57 100644 --- a/R-package/NAMESPACE +++ b/R-package/NAMESPACE @@ -34,6 +34,11 @@ export(slice) export(xgb.DMatrix) export(xgb.DMatrix.hasinfo) export(xgb.DMatrix.save) +export(xgb.DataIter) +export(xgb.ExternalDMatrix) +export(xgb.ProxyDMatrix) +export(xgb.QuantileDMatrix) +export(xgb.QuantileDMatrix.from_iterator) export(xgb.attr) export(xgb.attributes) export(xgb.config) diff --git a/R-package/R/xgb.DMatrix.R b/R-package/R/xgb.DMatrix.R index 7c4c30bd3..da036b952 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -1,13 +1,42 @@ #' Construct xgb.DMatrix object #' -#' Construct xgb.DMatrix object from either a dense matrix, a sparse matrix, or a local file. -#' Supported input file formats are either a LIBSVM text file or a binary file that was created previously by -#' \code{\link{xgb.DMatrix.save}}). +#' Construct an 'xgb.DMatrix' object from a given data source, which can then be passed to functions +#' such as \link{xgb.train} or \link{predict.xgb.Booster}. #' -#' @param data a \code{matrix} object (either numeric or integer), a \code{dgCMatrix} object, -#' 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. +#' Function 'xgb.QuantileDMatrix' will construct a DMatrix with quantization for the histogram +#' method already applied to it, which can be used to reduce memory usage (compared to using a +#' a regular DMatrix first and then creating a quantization out of it) when using the histogram +#' method (`tree_method = "hist"`, which is the default algorithm), but is not usable for the +#' sorted-indices method (`tree_method = "exact"`), nor for the approximate method +#' (`tree_method = "approx"`). +#' @param data Data from which to create a DMatrix, which can then be used for fitting models or +#' for getting predictions out of a fitted model. +#' +#' Supported input types are as follows:\itemize{ +#' \item `matrix` objects, with types `numeric`, `integer`, or `logical`. +#' \item `data.frame` objects, with columns of types `numeric`, `integer`, `logical`, or `factor`. +#' +#' If passing `enable_categorical=TRUE`, columns with `factor` type will be treated as categorical. +#' Otherwise, if passing `enable_categorical=FALSE` and the data contains `factor` columns, an error +#' will be thrown. +#' +#' Note that xgboost uses base-0 encoding for categorical types, hence `factor` types (which use base-1 +#' encoding') will be converted inside the function call. Be aware that the encoding used for `factor` +#' types is not kept as part of the model, so in subsequent calls to `predict`, it is the user's +#' responsibility to ensure that factor columns have the same levels as the ones from which the DMatrix +#' was constructed. +#' +#' Other column types are not supported. +#' \item CSR matrices, as class `dgRMatrix` from package `Matrix`. +#' \item CSC matrices, as class `dgCMatrix` from package `Matrix`. These are \bold{not} supported for +#' 'xgb.QuantileDMatrix'. +#' \item Single-row CSR matrices, as class `dsparseVector` from package `Matrix`, which is interpreted +#' as a single row (only when making predictions from a fitted model). +#' \item Text files in SVMLight / LibSVM formats, passed as a path to the file. These are \bold{not} +#' supported for xgb.QuantileDMatrix'. +#' \item Binary files generated by \link{xgb.DMatrix.save}, passed as a path to the file. These are +#' \bold{not} supported for xgb.QuantileDMatrix'. +#' } #' @param label Label of the training data. #' @param weight Weight for each instance. #' @@ -18,11 +47,32 @@ #' @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 missing A float value to represents missing values in data (not used when creating DMatrix +#' from text files). +#' It is useful to change when a zero, infinite, 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. Overrides column names in data #' frame and matrix. +#' +#' Note: columns are not referenced by name when calling `predict`, so the column order there +#' must be the same as in the DMatrix construction, regardless of the column names. +#' @param feature_types Set types for features. +#' +#' If `data` is a `data.frame` and passing `enable_categorical=TRUE`, the types will be deduced +#' automatically from the column types. +#' +#' Otherwise, one can pass a character vector with the same length as number of columns in `data`, +#' with the following possible values:\itemize{ +#' \item "c", which represents categorical columns. +#' \item "q", which represents numeric columns. +#' \item "int", which represents integer columns. +#' \item "i", which represents logical (boolean) columns. +#' } +#' +#' Note that, while categorical types are treated differently from the rest for model fitting +#' purposes, the other types do not influence the generated model, but have effects in other +#' functionalities such as feature importances. #' @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. @@ -41,6 +91,8 @@ #' If 'data' is not a data frame, this argument is ignored. #' #' JSON/UBJSON serialization format is required for this. +#' @return An 'xgb.DMatrix' object. If calling 'xgb.QuantileDMatrix', it will have additional +#' subclass 'xgb.QuantileDMatrix'. #' #' @details #' Note that DMatrix objects are not serializable through R functions such as \code{saveRDS} or \code{save}. @@ -60,6 +112,7 @@ #' xgb.DMatrix.save(dtrain, fname) #' dtrain <- xgb.DMatrix(fname) #' @export +#' @rdname xgb.DMatrix xgb.DMatrix <- function( data, label = NULL, @@ -68,6 +121,7 @@ xgb.DMatrix <- function( missing = NA, silent = FALSE, feature_names = colnames(data), + feature_types = NULL, nthread = NULL, group = NULL, qid = NULL, @@ -79,7 +133,7 @@ xgb.DMatrix <- function( if (!is.null(group) && !is.null(qid)) { stop("Either one of 'group' or 'qid' should be NULL") } - ctypes <- NULL + nthread <- as.integer(NVL(nthread, -1L)) if (typeof(data) == "character") { if (length(data) > 1) { stop( @@ -91,7 +145,7 @@ xgb.DMatrix <- function( handle <- .Call(XGDMatrixCreateFromFile_R, data, as.integer(silent)) } else if (is.matrix(data)) { handle <- .Call( - XGDMatrixCreateFromMat_R, data, missing, as.integer(NVL(nthread, -1)) + XGDMatrixCreateFromMat_R, data, missing, nthread ) } else if (inherits(data, "dgCMatrix")) { handle <- .Call( @@ -101,7 +155,7 @@ xgb.DMatrix <- function( data@x, nrow(data), missing, - as.integer(NVL(nthread, -1)) + nthread ) } else if (inherits(data, "dgRMatrix")) { handle <- .Call( @@ -111,7 +165,7 @@ xgb.DMatrix <- function( data@x, ncol(data), missing, - as.integer(NVL(nthread, -1)) + nthread ) } else if (inherits(data, "dsparseVector")) { indptr <- c(0L, as.integer(length(data@i))) @@ -123,41 +177,15 @@ xgb.DMatrix <- function( data@x, length(data), missing, - as.integer(NVL(nthread, -1)) + nthread ) } 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 - } - })) + tmp <- .process.df.for.dmatrix(data, enable_categorical, feature_types) + feature_types <- tmp$feature_types handle <- .Call( - XGDMatrixCreateFromDF_R, data, missing, as.integer(NVL(nthread, -1)) + XGDMatrixCreateFromDF_R, tmp$lst, missing, nthread ) + rm(tmp) } else { stop("xgb.DMatrix does not support construction from ", typeof(data)) } @@ -167,7 +195,81 @@ xgb.DMatrix <- function( class = "xgb.DMatrix", fields = new.env() ) + .set.dmatrix.fields( + dmat = dmat, + label = label, + weight = weight, + base_margin = base_margin, + feature_names = feature_names, + feature_types = feature_types, + group = group, + qid = qid, + label_lower_bound = label_lower_bound, + label_upper_bound = label_upper_bound, + feature_weights = feature_weights + ) + return(dmat) +} + +.process.df.for.dmatrix <- function(df, enable_categorical, feature_types) { + if (!nrow(df) || !ncol(df)) { + stop("'data' is an empty data.frame.") + } + if (!is.null(feature_types)) { + if (!is.character(feature_types) || length(feature_types) != ncol(df)) { + stop( + "'feature_types' must be a character vector with one entry per column in 'data'." + ) + } + } else { + feature_types <- sapply(df, function(col) { + if (is.factor(col)) { + if (!enable_categorical) { + stop( + "When factor type is used, the parameter `enable_categorical`", + " must be set to TRUE." + ) + } + return("c") + } else if (is.integer(col)) { + return("int") + } else if (is.logical(col)) { + return("i") + } else { + if (!is.numeric(col)) { + stop("Invalid type in dataframe.") + } + return("float") + } + }) + } + + lst <- lapply(df, function(col) { + is_factor <- is.factor(col) + col <- as.numeric(col) + if (is_factor) { + col <- col - 1 + } + return(col) + }) + + return(list(lst = lst, feature_types = feature_types)) +} + +.set.dmatrix.fields <- function( + dmat, + label, + weight, + base_margin, + feature_names, + feature_types, + group, + qid, + label_lower_bound, + label_upper_bound, + feature_weights +) { if (!is.null(label)) { setinfo(dmat, "label", label) } @@ -180,6 +282,9 @@ xgb.DMatrix <- function( if (!is.null(feature_names)) { setinfo(dmat, "feature_name", feature_names) } + if (!is.null(feature_types)) { + setinfo(dmat, "feature_type", feature_types) + } if (!is.null(group)) { setinfo(dmat, "group", group) } @@ -195,10 +300,515 @@ xgb.DMatrix <- function( if (!is.null(feature_weights)) { setinfo(dmat, "feature_weights", feature_weights) } - if (!is.null(ctypes)) { - setinfo(dmat, "feature_type", ctypes) +} + +#' @param ref The training dataset that provides quantile information, needed when creating +#' validation/test dataset with `xgb.QuantileDMatrix`. Supplying the training DMatrix +#' as a reference means that the same quantisation applied to the training data is +#' applied to the validation/test data +#' @param max_bin The number of histogram bin, should be consistent with the training parameter +#' `max_bin`. +#' +#' This is only supported when constructing a QuantileDMatrix. +#' @export +#' @rdname xgb.DMatrix +xgb.QuantileDMatrix <- function( + data, + label = NULL, + weight = NULL, + base_margin = NULL, + missing = NA, + feature_names = colnames(data), + feature_types = NULL, + nthread = NULL, + group = NULL, + qid = NULL, + label_lower_bound = NULL, + label_upper_bound = NULL, + feature_weights = NULL, + enable_categorical = FALSE, + ref = NULL, + max_bin = NULL +) { + nthread <- as.integer(NVL(nthread, -1L)) + if (!is.null(ref) && !inherits(ref, "xgb.DMatrix")) { + stop("'ref' must be an xgb.DMatrix object.") } + # Note: when passing an integer matrix, it won't get casted to numeric. + # Since 'int' values as understood by languages like C cannot have missing values, + # R represents missingness there by assigning them a value equal to the minimum + # integer. The 'missing' value here is set before the data, so in case of integers, + # need to make the conversion manually beforehand. + if (is.matrix(data) && storage.mode(data) %in% c("integer", "logical") && is.na(missing)) { + missing <- .Call(XGGetRNAIntAsDouble) + } + + iterator_env <- as.environment( + list( + data = data, + label = label, + weight = weight, + base_margin = base_margin, + missing = missing, + feature_names = feature_names, + feature_types = feature_types, + group = group, + qid = qid, + label_lower_bound = label_lower_bound, + label_upper_bound = label_upper_bound, + feature_weights = feature_weights, + enable_categorical = enable_categorical + ) + ) + data_iterator <- .single.data.iterator(iterator_env) + + # Note: the ProxyDMatrix has its finalizer assigned in the R externalptr + # object, but that finalizer will only be called once the object is + # garbage-collected, which doesn't happen immediately after it goes out + # of scope, hence this piece of code to tigger its destruction earlier + # and free memory right away. + proxy_handle <- .make.proxy.handle() + on.exit({ + .Call(XGDMatrixFree_R, proxy_handle) + }) + iterator_next <- function() { + return(xgb.ProxyDMatrix.internal(proxy_handle, data_iterator)) + } + iterator_reset <- function() { + return(data_iterator$f_reset(iterator_env)) + } + calling_env <- environment() + + dmat <- .Call( + XGQuantileDMatrixCreateFromCallback_R, + iterator_next, + iterator_reset, + calling_env, + proxy_handle, + nthread, + missing, + max_bin, + ref + ) + attributes(dmat) <- list( + class = c("xgb.DMatrix", "xgb.QuantileDMatrix"), + fields = attributes(proxy_handle)$fields + ) + return(dmat) +} + +#' @title XGBoost Data Iterator +#' @description Interface to create a custom data iterator in order to construct a DMatrix +#' from external memory. +#' +#' This function is responsible for generating an R object structure containing callback +#' functions and an environment shared with them. +#' +#' The output structure from this function is then meant to be passed to \link{xgb.ExternalDMatrix}, +#' which will consume the data and create a DMatrix from it by executing the callback functions. +#' +#' For more information, and for a usage example, see the documentation for \link{xgb.ExternalDMatrix}. +#' @param env An R environment to pass to the callback functions supplied here, which can be +#' used to keep track of variables to determine how to handle the batches. +#' +#' For example, one might want to keep track of an iteration number in this environment in order +#' to know which part of the data to pass next. +#' @param f_next `function(env)` which is responsible for:\itemize{ +#' \item Accessing or retrieving the next batch of data in the iterator. +#' \item Supplying this data by calling function \link{xgb.ProxyDMatrix} on it and returning the result. +#' \item Keeping track of where in the iterator batch it is or will go next, which can for example +#' be done by modifiying variables in the `env` variable that is passed here. +#' \item Signaling whether there are more batches to be consumed or not, by returning `NULL` +#' when the stream of data ends (all batches in the iterator have been consumed), or the result from +#' calling \link{xgb.ProxyDMatrix} when there are more batches in the line to be consumed. +#' } +#' @param f_reset `function(env)` which is responsible for reseting the data iterator +#' (i.e. taking it back to the first batch, called before and after the sequence of batches +#' has been consumed). +#' +#' Note that, after resetting the iterator, the batches will be accessed again, so the same data +#' (and in the same order) must be passed in subsequent iterations. +#' @return An `xgb.DataIter` object, containing the same inputs supplied here, which can then +#' be passed to \link{xgb.ExternalDMatrix}. +#' @seealso \link{xgb.ExternalDMatrix}, \link{xgb.ProxyDMatrix}. +#' @export +xgb.DataIter <- function(env = new.env(), f_next, f_reset) { + if (!is.function(f_next)) { + stop("'f_next' must be a function.") + } + if (!is.function(f_reset)) { + stop("'f_reset' must be a function.") + } + out <- list( + env = env, + f_next = f_next, + f_reset = f_reset + ) + class(out) <- "xgb.DataIter" + return(out) +} + +.qdm.single.fnext <- function(env) { + curr_iter <- env[["iter"]] + if (curr_iter >= 1L) { + return(NULL) + } + + on.exit({ + env[["iter"]] <- curr_iter + 1L + }) + return( + xgb.ProxyDMatrix( + data = env[["data"]], + label = env[["label"]], + weight = env[["weight"]], + base_margin = env[["base_margin"]], + feature_names = env[["feature_names"]], + feature_types = env[["feature_types"]], + group = env[["group"]], + qid = env[["qid"]], + label_lower_bound = env[["label_lower_bound"]], + label_upper_bound = env[["label_upper_bound"]], + feature_weights = env[["feature_weights"]], + enable_categorical = env[["enable_categorical"]] + ) + ) +} + +.qdm.single.freset <- function(env) { + env[["iter"]] <- 0L + return(invisible(NULL)) +} + +.single.data.iterator <- function(env) { + env[["iter"]] <- 0L + return(xgb.DataIter(env, .qdm.single.fnext, .qdm.single.freset)) +} + +# Only for internal usage +.make.proxy.handle <- function() { + out <- .Call(XGProxyDMatrixCreate_R) + attributes(out) <- list( + class = c("xgb.DMatrix", "xgb.ProxyDMatrixHandle"), + fields = new.env() + ) + return(out) +} + +#' @title Proxy DMatrix Updater +#' @description Helper function to supply data in batches of a data iterator when +#' constructing a DMatrix from external memory through \link{xgb.ExternalDMatrix} +#' or through \link{xgb.QuantileDMatrix.from_iterator}. +#' +#' This function is \bold{only} meant to be called inside of a callback function (which +#' is passed as argument to function \link{xgb.DataIter} to construct a data iterator) +#' when constructing a DMatrix through external memory - otherwise, one should call +#' \link{xgb.DMatrix} or \link{xgb.QuantileDMatrix}. +#' +#' The object that results from calling this function directly is \bold{not} like the other +#' `xgb.DMatrix` variants - i.e. cannot be used to train a model, nor to get predictions - only +#' possible usage is to supply data to an iterator, from which a DMatrix is then constructed. +#' +#' For more information and for example usage, see the documentation for \link{xgb.ExternalDMatrix}. +#' @inheritParams xgb.DMatrix +#' @param data Batch of data belonging to this batch. +#' +#' Note that not all of the input types supported by \link{xgb.DMatrix} are possible +#' to pass here. Supported types are:\itemize{ +#' \item `matrix`, with types `numeric`, `integer`, and `logical`. Note that for types +#' `integer` and `logical`, missing values might not be automatically recognized as +#' as such - see the documentation for parameter `missing` in \link{xgb.ExternalDMatrix} +#' for details on this. +#' \item `data.frame`, with the same types as supported by 'xgb.DMatrix' and same +#' conversions applied to it. See the documentation for parameter `data` in +#' \link{xgb.DMatrix} for details on it. +#' \item CSR matrices, as class `dgRMatrix` from package `Matrix`. +#' } +#' @return An object of class `xgb.ProxyDMatrix`, which is just a list containing the +#' data and parameters passed here. It does \bold{not} inherit from `xgb.DMatrix`. +#' @seealso \link{xgb.DataIter}, \link{xgb.ExternalDMatrix}. +#' @export +xgb.ProxyDMatrix <- function( + data, + label = NULL, + weight = NULL, + base_margin = NULL, + feature_names = colnames(data), + feature_types = NULL, + group = NULL, + qid = NULL, + label_lower_bound = NULL, + label_upper_bound = NULL, + feature_weights = NULL, + enable_categorical = FALSE +) { + stopifnot(inherits(data, c("matrix", "data.frame", "dgRMatrix"))) + out <- list( + data = data, + label = label, + weight = weight, + base_margin = base_margin, + feature_names = feature_names, + feature_types = feature_types, + group = group, + qid = qid, + label_lower_bound = label_lower_bound, + label_upper_bound = label_upper_bound, + feature_weights = feature_weights, + enable_categorical = enable_categorical + ) + class(out) <- "xgb.ProxyDMatrix" + return(out) +} + +xgb.ProxyDMatrix.internal <- function(proxy_handle, data_iterator) { + lst <- data_iterator$f_next(data_iterator$env) + if (is.null(lst)) { + return(0L) + } + if (!inherits(lst, "xgb.ProxyDMatrix")) { + stop("DataIter 'f_next' must return either NULL or the result from calling 'xgb.ProxyDMatrix'.") + } + + if (!is.null(lst$group) && !is.null(lst$qid)) { + stop("Either one of 'group' or 'qid' should be NULL") + } + if (is.data.frame(lst$data)) { + tmp <- .process.df.for.dmatrix(lst$data, lst$enable_categorical, lst$feature_types) + lst$feature_types <- tmp$feature_types + .Call(XGProxyDMatrixSetDataColumnar_R, proxy_handle, tmp$lst) + rm(tmp) + } else if (is.matrix(lst$data)) { + .Call(XGProxyDMatrixSetDataDense_R, proxy_handle, lst$data) + } else if (inherits(lst$data, "dgRMatrix")) { + tmp <- list(p = lst$data@p, j = lst$data@j, x = lst$data@x, ncol = ncol(lst$data)) + .Call(XGProxyDMatrixSetDataCSR_R, proxy_handle, tmp) + } else { + stop("'data' has unsupported type.") + } + + .set.dmatrix.fields( + dmat = proxy_handle, + label = lst$label, + weight = lst$weight, + base_margin = lst$base_margin, + feature_names = lst$feature_names, + feature_types = lst$feature_types, + group = lst$group, + qid = lst$qid, + label_lower_bound = lst$label_lower_bound, + label_upper_bound = lst$label_upper_bound, + feature_weights = lst$feature_weights + ) + + return(1L) +} + +#' @title DMatrix from External Data +#' @description Create a special type of xgboost 'DMatrix' object from external data +#' supplied by an \link{xgb.DataIter} object, potentially passed in batches from a +#' bigger set that might not fit entirely in memory. +#' +#' The data supplied by the iterator is accessed on-demand as needed, multiple times, +#' without being concatenated, but note that fields like 'label' \bold{will} be +#' concatenated from multiple calls to the data iterator. +#' +#' For more information, see the guide 'Using XGBoost External Memory Version': +#' \url{https://xgboost.readthedocs.io/en/stable/tutorials/external_memory.html} +#' @inheritParams xgb.DMatrix +#' @param data_iterator A data iterator structure as returned by \link{xgb.DataIter}, +#' which includes an environment shared between function calls, and functions to access +#' the data in batches on-demand. +#' @param cache_prefix The path of cache file, caller must initialize all the directories in this path. +#' @param missing A float value to represents missing values in data. +#' +#' Note that, while functions like \link{xgb.DMatrix} can take a generic `NA` and interpret it +#' correctly for different types like `numeric` and `integer`, if an `NA` value is passed here, +#' it will not be adapted for different input types. +#' +#' For example, in R `integer` types, missing values are represented by integer number `-2147483648` +#' (since machine 'integer' types do not have an inherent 'NA' value) - hence, if one passes `NA`, +#' which is interpreted as a floating-point NaN by 'xgb.ExternalDMatrix' and by +#' 'xgb.QuantileDMatrix.from_iterator', these integer missing values will not be treated as missing. +#' This should not pose any problem for `numeric` types, since they do have an inheret NaN value. +#' @return An 'xgb.DMatrix' object, with subclass 'xgb.ExternalDMatrix', in which the data is not +#' held internally but accessed through the iterator when needed. +#' @seealso \link{xgb.DataIter}, \link{xgb.ProxyDMatrix}, \link{xgb.QuantileDMatrix.from_iterator} +#' @examples +#' library(xgboost) +#' data(mtcars) +#' +#' # this custom environment will be passed to the iterator +#' # functions at each call. It's up to the user to keep +#' # track of the iteration number in this environment. +#' iterator_env <- as.environment( +#' list( +#' iter = 0, +#' x = mtcars[, -1], +#' y = mtcars[, 1] +#' ) +#' ) +#' +#' # Data is passed in two batches. +#' # In this example, batches are obtained by subsetting the 'x' variable. +#' # This is not advantageous to do, since the data is already loaded in memory +#' # and can be passed in full in one go, but there can be situations in which +#' # only a subset of the data will fit in the computer's memory, and it can +#' # be loaded in batches that are accessed one-at-a-time only. +#' iterator_next <- function(iterator_env) { +#' curr_iter <- iterator_env[["iter"]] +#' if (curr_iter >= 2) { +#' # there are only two batches, so this signals end of the stream +#' return(NULL) +#' } +#' +#' if (curr_iter == 0) { +#' x_batch <- iterator_env[["x"]][1:16, ] +#' y_batch <- iterator_env[["y"]][1:16] +#' } else { +#' x_batch <- iterator_env[["x"]][17:32, ] +#' y_batch <- iterator_env[["y"]][17:32] +#' } +#' on.exit({ +#' iterator_env[["iter"]] <- curr_iter + 1 +#' }) +#' +#' # Function 'xgb.ProxyDMatrix' must be called manually +#' # at each batch with all the appropriate attributes, +#' # such as feature names and feature types. +#' return(xgb.ProxyDMatrix(data = x_batch, label = y_batch)) +#' } +#' +#' # This moves the iterator back to its beginning +#' iterator_reset <- function(iterator_env) { +#' iterator_env[["iter"]] <- 0 +#' } +#' +#' data_iterator <- xgb.DataIter( +#' env = iterator_env, +#' f_next = iterator_next, +#' f_reset = iterator_reset +#' ) +#' cache_prefix <- tempdir() +#' +#' # DMatrix will be constructed from the iterator's batches +#' dm <- xgb.ExternalDMatrix(data_iterator, cache_prefix, nthread = 1) +#' +#' # After construction, can be used as a regular DMatrix +#' params <- list(nthread = 1, objective = "reg:squarederror") +#' model <- xgb.train(data = dm, nrounds = 2, params = params) +#' +#' # Predictions can also be called on it, and should be the same +#' # as if the data were passed differently. +#' pred_dm <- predict(model, dm) +#' pred_mat <- predict(model, as.matrix(mtcars[, -1])) +#' @export +xgb.ExternalDMatrix <- function( + data_iterator, + cache_prefix = tempdir(), + missing = NA, + nthread = NULL +) { + stopifnot(inherits(data_iterator, "xgb.DataIter")) + stopifnot(is.character(cache_prefix)) + + cache_prefix <- path.expand(cache_prefix) + nthread <- as.integer(NVL(nthread, -1L)) + + proxy_handle <- .make.proxy.handle() + on.exit({ + .Call(XGDMatrixFree_R, proxy_handle) + }) + iterator_next <- function() { + return(xgb.ProxyDMatrix.internal(proxy_handle, data_iterator)) + } + iterator_reset <- function() { + return(data_iterator$f_reset(data_iterator$env)) + } + calling_env <- environment() + + dmat <- .Call( + XGDMatrixCreateFromCallback_R, + iterator_next, + iterator_reset, + calling_env, + proxy_handle, + nthread, + missing, + cache_prefix + ) + + attributes(dmat) <- list( + class = c("xgb.DMatrix", "xgb.ExternalDMatrix"), + fields = attributes(proxy_handle)$fields + ) + return(dmat) +} + + +#' @title QuantileDMatrix from External Data +#' @description Create an `xgb.QuantileDMatrix` object (exact same class as would be returned by +#' calling function \link{xgb.QuantileDMatrix}, with the same advantages and limitations) from +#' external data supplied by an \link{xgb.DataIter} object, potentially passed in batches from +#' a bigger set that might not fit entirely in memory, same way as \link{xgb.ExternalDMatrix}. +#' +#' Note that, while external data will only be loaded through the iterator (thus the full data +#' might not be held entirely in-memory), the quantized representation of the data will get +#' created in-memory, being concatenated from multiple calls to the data iterator. The quantized +#' version is typically lighter than the original data, so there might be cases in which this +#' representation could potentially fit in memory even if the full data doesn't. +#' +#' For more information, see the guide 'Using XGBoost External Memory Version': +#' \url{https://xgboost.readthedocs.io/en/stable/tutorials/external_memory.html} +#' @inheritParams xgb.ExternalDMatrix +#' @inheritParams xgb.QuantileDMatrix +#' @return An 'xgb.DMatrix' object, with subclass 'xgb.QuantileDMatrix'. +#' @seealso \link{xgb.DataIter}, \link{xgb.ProxyDMatrix}, \link{xgb.ExternalDMatrix}, +#' \link{xgb.QuantileDMatrix} +#' @export +xgb.QuantileDMatrix.from_iterator <- function( # nolint + data_iterator, + missing = NA, + nthread = NULL, + ref = NULL, + max_bin = NULL +) { + stopifnot(inherits(data_iterator, "xgb.DataIter")) + if (!is.null(ref) && !inherits(ref, "xgb.DMatrix")) { + stop("'ref' must be an xgb.DMatrix object.") + } + + nthread <- as.integer(NVL(nthread, -1L)) + + proxy_handle <- .make.proxy.handle() + on.exit({ + .Call(XGDMatrixFree_R, proxy_handle) + }) + iterator_next <- function() { + return(xgb.ProxyDMatrix.internal(proxy_handle, data_iterator)) + } + iterator_reset <- function() { + return(data_iterator$f_reset(data_iterator$env)) + } + calling_env <- environment() + + dmat <- .Call( + XGQuantileDMatrixCreateFromCallback_R, + iterator_next, + iterator_reset, + calling_env, + proxy_handle, + nthread, + missing, + max_bin, + ref + ) + + attributes(dmat) <- list( + class = c("xgb.DMatrix", "xgb.QuantileDMatrix"), + fields = attributes(proxy_handle)$fields + ) return(dmat) } @@ -712,7 +1322,17 @@ print.xgb.DMatrix <- function(x, verbose = FALSE, ...) { cat("INVALID xgb.DMatrix object. Must be constructed anew.\n") return(invisible(x)) } - cat('xgb.DMatrix dim:', nrow(x), 'x', ncol(x), ' info: ') + class_print <- if (inherits(x, "xgb.QuantileDMatrix")) { + "xgb.QuantileDMatrix" + } else if (inherits(x, "xgb.ExternalDMatrix")) { + "xgb.ExternalDMatrix" + } else if (inherits(x, "xgb.ProxyDMatrix")) { + "xgb.ProxyDMatrix" + } else { + "xgb.DMatrix" + } + + cat(class_print, ' dim:', nrow(x), 'x', ncol(x), ' info: ') infos <- character(0) if (xgb.DMatrix.hasinfo(x, 'label')) infos <- 'label' if (xgb.DMatrix.hasinfo(x, 'weight')) infos <- c(infos, 'weight') diff --git a/R-package/man/xgb.DMatrix.Rd b/R-package/man/xgb.DMatrix.Rd index eb667377f..ceb60dc42 100644 --- a/R-package/man/xgb.DMatrix.Rd +++ b/R-package/man/xgb.DMatrix.Rd @@ -2,6 +2,7 @@ % Please edit documentation in R/xgb.DMatrix.R \name{xgb.DMatrix} \alias{xgb.DMatrix} +\alias{xgb.QuantileDMatrix} \title{Construct xgb.DMatrix object} \usage{ xgb.DMatrix( @@ -12,6 +13,7 @@ xgb.DMatrix( missing = NA, silent = FALSE, feature_names = colnames(data), + feature_types = NULL, nthread = NULL, group = NULL, qid = NULL, @@ -20,12 +22,55 @@ xgb.DMatrix( feature_weights = NULL, enable_categorical = FALSE ) + +xgb.QuantileDMatrix( + data, + label = NULL, + weight = NULL, + base_margin = NULL, + missing = NA, + feature_names = colnames(data), + feature_types = NULL, + nthread = NULL, + group = NULL, + qid = NULL, + label_lower_bound = NULL, + label_upper_bound = NULL, + feature_weights = NULL, + enable_categorical = FALSE, + ref = NULL, + max_bin = NULL +) } \arguments{ -\item{data}{a \code{matrix} object (either numeric or integer), a \code{dgCMatrix} object, -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{data}{Data from which to create a DMatrix, which can then be used for fitting models or +for getting predictions out of a fitted model. + +Supported input types are as follows:\itemize{ +\item \code{matrix} objects, with types \code{numeric}, \code{integer}, or \code{logical}. +\item \code{data.frame} objects, with columns of types \code{numeric}, \code{integer}, \code{logical}, or \code{factor}. + +If passing \code{enable_categorical=TRUE}, columns with \code{factor} type will be treated as categorical. +Otherwise, if passing \code{enable_categorical=FALSE} and the data contains \code{factor} columns, an error +will be thrown. + +Note that xgboost uses base-0 encoding for categorical types, hence \code{factor} types (which use base-1 +encoding') will be converted inside the function call. Be aware that the encoding used for \code{factor} +types is not kept as part of the model, so in subsequent calls to \code{predict}, it is the user's +responsibility to ensure that factor columns have the same levels as the ones from which the DMatrix +was constructed. + +Other column types are not supported. +\item CSR matrices, as class \code{dgRMatrix} from package \code{Matrix}. +\item CSC matrices, as class \code{dgCMatrix} from package \code{Matrix}. These are \bold{not} supported for +'xgb.QuantileDMatrix'. +\item Single-row CSR matrices, as class \code{dsparseVector} from package \code{Matrix}, which is interpreted +as a single row (only when making predictions from a fitted model). +\item Text files in SVMLight / LibSVM formats, passed as a path to the file. These are \bold{not} +supported for xgb.QuantileDMatrix'. +\item Binary files generated by \link{xgb.DMatrix.save}, passed as a path to the file. These are +\bold{not} supported for xgb.QuantileDMatrix'. +}} \item{label}{Label of the training data.} @@ -41,13 +86,36 @@ so it doesn't make sense to assign weights to individual data points.} \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.} +\item{missing}{A float value to represents missing values in data (not used when creating DMatrix +from text files). +It is useful to change when a zero, infinite, 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. Overrides column names in data -frame and matrix.} +frame and matrix. + +\if{html}{\out{
}}\preformatted{ Note: columns are not referenced by name when calling `predict`, so the column order there + must be the same as in the DMatrix construction, regardless of the column names. +}\if{html}{\out{
}}} + +\item{feature_types}{Set types for features. + +If \code{data} is a \code{data.frame} and passing \code{enable_categorical=TRUE}, the types will be deduced +automatically from the column types. + +Otherwise, one can pass a character vector with the same length as number of columns in \code{data}, +with the following possible values:\itemize{ +\item "c", which represents categorical columns. +\item "q", which represents numeric columns. +\item "int", which represents integer columns. +\item "i", which represents logical (boolean) columns. +} + +Note that, while categorical types are treated differently from the rest for model fitting +purposes, the other types do not influence the generated model, but have effects in other +functionalities such as feature importances.} \item{nthread}{Number of threads used for creating DMatrix.} @@ -74,13 +142,33 @@ frame and matrix.} JSON/UBJSON serialization format is required for this. }\if{html}{\out{}}} + +\item{ref}{The training dataset that provides quantile information, needed when creating +validation/test dataset with \code{xgb.QuantileDMatrix}. Supplying the training DMatrix +as a reference means that the same quantisation applied to the training data is +applied to the validation/test data} + +\item{max_bin}{The number of histogram bin, should be consistent with the training parameter +\code{max_bin}. + +This is only supported when constructing a QuantileDMatrix.} +} +\value{ +An 'xgb.DMatrix' object. If calling 'xgb.QuantileDMatrix', it will have additional +subclass 'xgb.QuantileDMatrix'. } \description{ -Construct xgb.DMatrix object from either a dense matrix, a sparse matrix, or a local file. -Supported input file formats are either a LIBSVM text file or a binary file that was created previously by -\code{\link{xgb.DMatrix.save}}). +Construct an 'xgb.DMatrix' object from a given data source, which can then be passed to functions +such as \link{xgb.train} or \link{predict.xgb.Booster}. } \details{ +Function 'xgb.QuantileDMatrix' will construct a DMatrix with quantization for the histogram +method already applied to it, which can be used to reduce memory usage (compared to using a +a regular DMatrix first and then creating a quantization out of it) when using the histogram +method (\code{tree_method = "hist"}, which is the default algorithm), but is not usable for the +sorted-indices method (\code{tree_method = "exact"}), nor for the approximate method +(\code{tree_method = "approx"}). + 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 diff --git a/R-package/man/xgb.DataIter.Rd b/R-package/man/xgb.DataIter.Rd new file mode 100644 index 000000000..29cf5acc9 --- /dev/null +++ b/R-package/man/xgb.DataIter.Rd @@ -0,0 +1,51 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/xgb.DMatrix.R +\name{xgb.DataIter} +\alias{xgb.DataIter} +\title{XGBoost Data Iterator} +\usage{ +xgb.DataIter(env = new.env(), f_next, f_reset) +} +\arguments{ +\item{env}{An R environment to pass to the callback functions supplied here, which can be +used to keep track of variables to determine how to handle the batches. + +For example, one might want to keep track of an iteration number in this environment in order +to know which part of the data to pass next.} + +\item{f_next}{\verb{function(env)} which is responsible for:\itemize{ +\item Accessing or retrieving the next batch of data in the iterator. +\item Supplying this data by calling function \link{xgb.ProxyDMatrix} on it and returning the result. +\item Keeping track of where in the iterator batch it is or will go next, which can for example +be done by modifiying variables in the \code{env} variable that is passed here. +\item Signaling whether there are more batches to be consumed or not, by returning \code{NULL} +when the stream of data ends (all batches in the iterator have been consumed), or the result from +calling \link{xgb.ProxyDMatrix} when there are more batches in the line to be consumed. +}} + +\item{f_reset}{\verb{function(env)} which is responsible for reseting the data iterator +(i.e. taking it back to the first batch, called before and after the sequence of batches +has been consumed). + +Note that, after resetting the iterator, the batches will be accessed again, so the same data +(and in the same order) must be passed in subsequent iterations.} +} +\value{ +An \code{xgb.DataIter} object, containing the same inputs supplied here, which can then +be passed to \link{xgb.ExternalDMatrix}. +} +\description{ +Interface to create a custom data iterator in order to construct a DMatrix +from external memory. + +This function is responsible for generating an R object structure containing callback +functions and an environment shared with them. + +The output structure from this function is then meant to be passed to \link{xgb.ExternalDMatrix}, +which will consume the data and create a DMatrix from it by executing the callback functions. + +For more information, and for a usage example, see the documentation for \link{xgb.ExternalDMatrix}. +} +\seealso{ +\link{xgb.ExternalDMatrix}, \link{xgb.ProxyDMatrix}. +} diff --git a/R-package/man/xgb.ExternalDMatrix.Rd b/R-package/man/xgb.ExternalDMatrix.Rd new file mode 100644 index 000000000..3e7844990 --- /dev/null +++ b/R-package/man/xgb.ExternalDMatrix.Rd @@ -0,0 +1,122 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/xgb.DMatrix.R +\name{xgb.ExternalDMatrix} +\alias{xgb.ExternalDMatrix} +\title{DMatrix from External Data} +\usage{ +xgb.ExternalDMatrix( + data_iterator, + cache_prefix = tempdir(), + missing = NA, + nthread = NULL +) +} +\arguments{ +\item{data_iterator}{A data iterator structure as returned by \link{xgb.DataIter}, +which includes an environment shared between function calls, and functions to access +the data in batches on-demand.} + +\item{cache_prefix}{The path of cache file, caller must initialize all the directories in this path.} + +\item{missing}{A float value to represents missing values in data. + +Note that, while functions like \link{xgb.DMatrix} can take a generic \code{NA} and interpret it +correctly for different types like \code{numeric} and \code{integer}, if an \code{NA} value is passed here, +it will not be adapted for different input types. + +For example, in R \code{integer} types, missing values are represented by integer number \code{-2147483648} +(since machine 'integer' types do not have an inherent 'NA' value) - hence, if one passes \code{NA}, +which is interpreted as a floating-point NaN by 'xgb.ExternalDMatrix' and by +'xgb.QuantileDMatrix.from_iterator', these integer missing values will not be treated as missing. +This should not pose any problem for \code{numeric} types, since they do have an inheret NaN value.} + +\item{nthread}{Number of threads used for creating DMatrix.} +} +\value{ +An 'xgb.DMatrix' object, with subclass 'xgb.ExternalDMatrix', in which the data is not +held internally but accessed through the iterator when needed. +} +\description{ +Create a special type of xgboost 'DMatrix' object from external data +supplied by an \link{xgb.DataIter} object, potentially passed in batches from a +bigger set that might not fit entirely in memory. + +The data supplied by the iterator is accessed on-demand as needed, multiple times, +without being concatenated, but note that fields like 'label' \bold{will} be +concatenated from multiple calls to the data iterator. + +For more information, see the guide 'Using XGBoost External Memory Version': +\url{https://xgboost.readthedocs.io/en/stable/tutorials/external_memory.html} +} +\examples{ +library(xgboost) +data(mtcars) + +# this custom environment will be passed to the iterator +# functions at each call. It's up to the user to keep +# track of the iteration number in this environment. +iterator_env <- as.environment( + list( + iter = 0, + x = mtcars[, -1], + y = mtcars[, 1] + ) +) + +# Data is passed in two batches. +# In this example, batches are obtained by subsetting the 'x' variable. +# This is not advantageous to do, since the data is already loaded in memory +# and can be passed in full in one go, but there can be situations in which +# only a subset of the data will fit in the computer's memory, and it can +# be loaded in batches that are accessed one-at-a-time only. +iterator_next <- function(iterator_env) { + curr_iter <- iterator_env[["iter"]] + if (curr_iter >= 2) { + # there are only two batches, so this signals end of the stream + return(NULL) + } + + if (curr_iter == 0) { + x_batch <- iterator_env[["x"]][1:16, ] + y_batch <- iterator_env[["y"]][1:16] + } else { + x_batch <- iterator_env[["x"]][17:32, ] + y_batch <- iterator_env[["y"]][17:32] + } + on.exit({ + iterator_env[["iter"]] <- curr_iter + 1 + }) + + # Function 'xgb.ProxyDMatrix' must be called manually + # at each batch with all the appropriate attributes, + # such as feature names and feature types. + return(xgb.ProxyDMatrix(data = x_batch, label = y_batch)) +} + +# This moves the iterator back to its beginning +iterator_reset <- function(iterator_env) { + iterator_env[["iter"]] <- 0 +} + +data_iterator <- xgb.DataIter( + env = iterator_env, + f_next = iterator_next, + f_reset = iterator_reset +) +cache_prefix <- tempdir() + +# DMatrix will be constructed from the iterator's batches +dm <- xgb.ExternalDMatrix(data_iterator, cache_prefix, nthread = 1) + +# After construction, can be used as a regular DMatrix +params <- list(nthread = 1, objective = "reg:squarederror") +model <- xgb.train(data = dm, nrounds = 2, params = params) + +# Predictions can also be called on it, and should be the same +# as if the data were passed differently. +pred_dm <- predict(model, dm) +pred_mat <- predict(model, as.matrix(mtcars[, -1])) +} +\seealso{ +\link{xgb.DataIter}, \link{xgb.ProxyDMatrix}, \link{xgb.QuantileDMatrix.from_iterator} +} diff --git a/R-package/man/xgb.ProxyDMatrix.Rd b/R-package/man/xgb.ProxyDMatrix.Rd new file mode 100644 index 000000000..5a9b6251a --- /dev/null +++ b/R-package/man/xgb.ProxyDMatrix.Rd @@ -0,0 +1,121 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/xgb.DMatrix.R +\name{xgb.ProxyDMatrix} +\alias{xgb.ProxyDMatrix} +\title{Proxy DMatrix Updater} +\usage{ +xgb.ProxyDMatrix( + data, + label = NULL, + weight = NULL, + base_margin = NULL, + feature_names = colnames(data), + feature_types = NULL, + group = NULL, + qid = NULL, + label_lower_bound = NULL, + label_upper_bound = NULL, + feature_weights = NULL, + enable_categorical = FALSE +) +} +\arguments{ +\item{data}{Batch of data belonging to this batch. + +Note that not all of the input types supported by \link{xgb.DMatrix} are possible +to pass here. Supported types are:\itemize{ +\item \code{matrix}, with types \code{numeric}, \code{integer}, and \code{logical}. Note that for types +\code{integer} and \code{logical}, missing values might not be automatically recognized as +as such - see the documentation for parameter \code{missing} in \link{xgb.ExternalDMatrix} +for details on this. +\item \code{data.frame}, with the same types as supported by 'xgb.DMatrix' and same +conversions applied to it. See the documentation for parameter \code{data} in +\link{xgb.DMatrix} for details on it. +\item CSR matrices, as class \code{dgRMatrix} from package \code{Matrix}. +}} + +\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. + +\if{html}{\out{
}}\preformatted{ In the case of multi-output models, one can also pass multi-dimensional base_margin. +}\if{html}{\out{
}}} + +\item{feature_names}{Set names for features. Overrides column names in data +frame and matrix. + +\if{html}{\out{
}}\preformatted{ Note: columns are not referenced by name when calling `predict`, so the column order there + must be the same as in the DMatrix construction, regardless of the column names. +}\if{html}{\out{
}}} + +\item{feature_types}{Set types for features. + +If \code{data} is a \code{data.frame} and passing \code{enable_categorical=TRUE}, the types will be deduced +automatically from the column types. + +Otherwise, one can pass a character vector with the same length as number of columns in \code{data}, +with the following possible values:\itemize{ +\item "c", which represents categorical columns. +\item "q", which represents numeric columns. +\item "int", which represents integer columns. +\item "i", which represents logical (boolean) columns. +} + +Note that, while categorical types are treated differently from the rest for model fitting +purposes, the other types do not influence the generated model, but have effects in other +functionalities such as feature importances.} + +\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.} + +\item{enable_categorical}{Experimental support of specializing for categorical features. + +\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 'data' is not a data frame, this argument is ignored. + + JSON/UBJSON serialization format is required for this. +}\if{html}{\out{
}}} +} +\value{ +An object of class \code{xgb.ProxyDMatrix}, which is just a list containing the +data and parameters passed here. It does \bold{not} inherit from \code{xgb.DMatrix}. +} +\description{ +Helper function to supply data in batches of a data iterator when +constructing a DMatrix from external memory through \link{xgb.ExternalDMatrix} +or through \link{xgb.QuantileDMatrix.from_iterator}. + +This function is \bold{only} meant to be called inside of a callback function (which +is passed as argument to function \link{xgb.DataIter} to construct a data iterator) +when constructing a DMatrix through external memory - otherwise, one should call +\link{xgb.DMatrix} or \link{xgb.QuantileDMatrix}. + +The object that results from calling this function directly is \bold{not} like the other +\code{xgb.DMatrix} variants - i.e. cannot be used to train a model, nor to get predictions - only +possible usage is to supply data to an iterator, from which a DMatrix is then constructed. + +For more information and for example usage, see the documentation for \link{xgb.ExternalDMatrix}. +} +\seealso{ +\link{xgb.DataIter}, \link{xgb.ExternalDMatrix}. +} diff --git a/R-package/man/xgb.QuantileDMatrix.from_iterator.Rd b/R-package/man/xgb.QuantileDMatrix.from_iterator.Rd new file mode 100644 index 000000000..21f24576d --- /dev/null +++ b/R-package/man/xgb.QuantileDMatrix.from_iterator.Rd @@ -0,0 +1,65 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/xgb.DMatrix.R +\name{xgb.QuantileDMatrix.from_iterator} +\alias{xgb.QuantileDMatrix.from_iterator} +\title{QuantileDMatrix from External Data} +\usage{ +xgb.QuantileDMatrix.from_iterator( + data_iterator, + missing = NA, + nthread = NULL, + ref = NULL, + max_bin = NULL +) +} +\arguments{ +\item{data_iterator}{A data iterator structure as returned by \link{xgb.DataIter}, +which includes an environment shared between function calls, and functions to access +the data in batches on-demand.} + +\item{missing}{A float value to represents missing values in data. + +Note that, while functions like \link{xgb.DMatrix} can take a generic \code{NA} and interpret it +correctly for different types like \code{numeric} and \code{integer}, if an \code{NA} value is passed here, +it will not be adapted for different input types. + +For example, in R \code{integer} types, missing values are represented by integer number \code{-2147483648} +(since machine 'integer' types do not have an inherent 'NA' value) - hence, if one passes \code{NA}, +which is interpreted as a floating-point NaN by 'xgb.ExternalDMatrix' and by +'xgb.QuantileDMatrix.from_iterator', these integer missing values will not be treated as missing. +This should not pose any problem for \code{numeric} types, since they do have an inheret NaN value.} + +\item{nthread}{Number of threads used for creating DMatrix.} + +\item{ref}{The training dataset that provides quantile information, needed when creating +validation/test dataset with \code{xgb.QuantileDMatrix}. Supplying the training DMatrix +as a reference means that the same quantisation applied to the training data is +applied to the validation/test data} + +\item{max_bin}{The number of histogram bin, should be consistent with the training parameter +\code{max_bin}. + +This is only supported when constructing a QuantileDMatrix.} +} +\value{ +An 'xgb.DMatrix' object, with subclass 'xgb.QuantileDMatrix'. +} +\description{ +Create an \code{xgb.QuantileDMatrix} object (exact same class as would be returned by +calling function \link{xgb.QuantileDMatrix}, with the same advantages and limitations) from +external data supplied by an \link{xgb.DataIter} object, potentially passed in batches from +a bigger set that might not fit entirely in memory, same way as \link{xgb.ExternalDMatrix}. + +Note that, while external data will only be loaded through the iterator (thus the full data +might not be held entirely in-memory), the quantized representation of the data will get +created in-memory, being concatenated from multiple calls to the data iterator. The quantized +version is typically lighter than the original data, so there might be cases in which this +representation could potentially fit in memory even if the full data doesn't. + +For more information, see the guide 'Using XGBoost External Memory Version': +\url{https://xgboost.readthedocs.io/en/stable/tutorials/external_memory.html} +} +\seealso{ +\link{xgb.DataIter}, \link{xgb.ProxyDMatrix}, \link{xgb.ExternalDMatrix}, +\link{xgb.QuantileDMatrix} +} diff --git a/R-package/src/init.c b/R-package/src/init.c index fff5d9f90..a9f3f3e38 100644 --- a/R-package/src/init.c +++ b/R-package/src/init.c @@ -54,6 +54,14 @@ 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 XGProxyDMatrixCreate_R(); +extern SEXP XGProxyDMatrixSetDataDense_R(SEXP, SEXP); +extern SEXP XGProxyDMatrixSetDataCSR_R(SEXP, SEXP); +extern SEXP XGProxyDMatrixSetDataColumnar_R(SEXP, SEXP); +extern SEXP XGDMatrixCreateFromCallback_R(SEXP, SEXP, SEXP, SEXP, SEXP, SEXP, SEXP); +extern SEXP XGQuantileDMatrixCreateFromCallback_R(SEXP, SEXP, SEXP, SEXP, SEXP, SEXP, SEXP, SEXP); +extern SEXP XGDMatrixFree_R(SEXP); +extern SEXP XGGetRNAIntAsDouble(); extern SEXP XGDMatrixGetQuantileCut_R(SEXP); extern SEXP XGDMatrixNumNonMissing_R(SEXP); extern SEXP XGDMatrixGetDataAsCSR_R(SEXP); @@ -105,6 +113,14 @@ 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}, + {"XGProxyDMatrixCreate_R", (DL_FUNC) &XGProxyDMatrixCreate_R, 0}, + {"XGProxyDMatrixSetDataDense_R", (DL_FUNC) &XGProxyDMatrixSetDataDense_R, 2}, + {"XGProxyDMatrixSetDataCSR_R", (DL_FUNC) &XGProxyDMatrixSetDataCSR_R, 2}, + {"XGProxyDMatrixSetDataColumnar_R", (DL_FUNC) &XGProxyDMatrixSetDataColumnar_R, 2}, + {"XGDMatrixCreateFromCallback_R", (DL_FUNC) &XGDMatrixCreateFromCallback_R, 7}, + {"XGQuantileDMatrixCreateFromCallback_R", (DL_FUNC) &XGQuantileDMatrixCreateFromCallback_R, 8}, + {"XGDMatrixFree_R", (DL_FUNC) &XGDMatrixFree_R, 1}, + {"XGGetRNAIntAsDouble", (DL_FUNC) &XGGetRNAIntAsDouble, 0}, {"XGDMatrixGetQuantileCut_R", (DL_FUNC) &XGDMatrixGetQuantileCut_R, 1}, {"XGDMatrixNumNonMissing_R", (DL_FUNC) &XGDMatrixNumNonMissing_R, 1}, {"XGDMatrixGetDataAsCSR_R", (DL_FUNC) &XGDMatrixGetDataAsCSR_R, 1}, diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc index 1d01b9aae..c91fb94c4 100644 --- a/R-package/src/xgboost_R.cc +++ b/R-package/src/xgboost_R.cc @@ -27,7 +27,12 @@ #include "./xgboost_R.h" // Must follow other includes. namespace { -struct ErrorWithUnwind : public std::exception {}; + +/* Note: this class is used as a throwable exception. +Some xgboost C functions that use callbacks will catch exceptions +that happen inside of the callback execution, hence it purposefully +doesn't inherit from 'std::exception' even if used as such. */ +struct ErrorWithUnwind {}; void ThrowExceptionFromRError(void *, Rboolean jump) { if (jump) { @@ -51,6 +56,27 @@ SEXP SafeMkChar(const char *c_str, SEXP continuation_token) { continuation_token); } +struct RFunAndEnv { + SEXP R_fun; + SEXP R_calling_env; +}; + +SEXP WrappedExecFun(void *void_ptr) { + RFunAndEnv *r_fun_and_env = static_cast(void_ptr); + SEXP f_expr = Rf_protect(Rf_lang1(r_fun_and_env->R_fun)); + SEXP out = Rf_protect(Rf_eval(f_expr, r_fun_and_env->R_calling_env)); + Rf_unprotect(2); + return out; +} + +SEXP SafeExecFun(SEXP R_fun, SEXP R_calling_env, SEXP continuation_token) { + RFunAndEnv r_fun_and_env{R_fun, R_calling_env}; + return R_UnwindProtect( + WrappedExecFun, static_cast(&r_fun_and_env), + ThrowExceptionFromRError, nullptr, + continuation_token); +} + SEXP WrappedAllocReal(void *void_ptr) { size_t *size = static_cast(void_ptr); return Rf_allocVector(REALSXP, *size); @@ -140,6 +166,47 @@ SEXP SafeAllocInteger(size_t size, SEXP continuation_token) { return ""; } +[[nodiscard]] std::string MakeArrayInterfaceFromRDataFrame(SEXP R_df) { + auto make_vec = [&](auto const *ptr, std::size_t len) { + auto v = xgboost::linalg::MakeVec(ptr, len); + return xgboost::linalg::ArrayInterface(v); + }; + + R_xlen_t n_features = Rf_xlength(R_df); + std::vector array(n_features); + CHECK_GT(n_features, 0); + std::size_t len = Rf_xlength(VECTOR_ELT(R_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 (R_xlen_t i = 0; i < n_features; ++i) { + switch (TYPEOF(VECTOR_ELT(R_df, i))) { + case INTSXP: { + auto const *ptr = INTEGER(VECTOR_ELT(R_df, i)); + array[i] = make_vec(ptr, len); + break; + } + case REALSXP: { + auto const *ptr = REAL(VECTOR_ELT(R_df, i)); + array[i] = make_vec(ptr, len); + break; + } + case LGLSXP: { + auto const *ptr = LOGICAL(VECTOR_ELT(R_df, i)); + array[i] = make_vec(ptr, len); + break; + } + default: { + LOG(FATAL) << "data.frame has unsupported type."; + } + } + } + + xgboost::Json jinterface{std::move(array)}; + return xgboost::Json::Dump(jinterface); +} + [[nodiscard]] std::string MakeJsonConfigForArray(SEXP missing, SEXP n_threads, SEXPTYPE arr_type) { using namespace ::xgboost; // NOLINT Json jconfig{Object{}}; @@ -335,51 +402,13 @@ XGB_DLL SEXP XGDMatrixCreateFromDF_R(SEXP df, SEXP missing, SEXP n_threads) { 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{}}; + std::string sinterface = MakeArrayInterfaceFromRDataFrame(df); + xgboost::Json jconfig{xgboost::Object{}}; jconfig["missing"] = asReal(missing); jconfig["nthread"] = asInteger(n_threads); - auto sconfig = Json::Dump(jconfig); + std::string sconfig = xgboost::Json::Dump(jconfig); rc = XGDMatrixCreateFromColumnar(sinterface.c_str(), sconfig.c_str(), &handle); } @@ -632,6 +661,192 @@ XGB_DLL SEXP XGDMatrixNumCol_R(SEXP handle) { return ScalarInteger(static_cast(ncol)); } +XGB_DLL SEXP XGProxyDMatrixCreate_R() { + SEXP out = Rf_protect(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); + R_API_BEGIN(); + DMatrixHandle proxy_dmat_handle; + CHECK_CALL(XGProxyDMatrixCreate(&proxy_dmat_handle)); + R_SetExternalPtrAddr(out, proxy_dmat_handle); + R_RegisterCFinalizerEx(out, _DMatrixFinalizer, TRUE); + Rf_unprotect(1); + R_API_END(); + return out; +} + +XGB_DLL SEXP XGProxyDMatrixSetDataDense_R(SEXP handle, SEXP R_mat) { + R_API_BEGIN(); + DMatrixHandle proxy_dmat = R_ExternalPtrAddr(handle); + int res_code; + { + std::string array_str = MakeArrayInterfaceFromRMat(R_mat); + res_code = XGProxyDMatrixSetDataDense(proxy_dmat, array_str.c_str()); + } + CHECK_CALL(res_code); + R_API_END(); + return R_NilValue; +} + +XGB_DLL SEXP XGProxyDMatrixSetDataCSR_R(SEXP handle, SEXP lst) { + R_API_BEGIN(); + DMatrixHandle proxy_dmat = R_ExternalPtrAddr(handle); + int res_code; + { + std::string array_str_indptr = MakeArrayInterfaceFromRVector(VECTOR_ELT(lst, 0)); + std::string array_str_indices = MakeArrayInterfaceFromRVector(VECTOR_ELT(lst, 1)); + std::string array_str_data = MakeArrayInterfaceFromRVector(VECTOR_ELT(lst, 2)); + const int ncol = Rf_asInteger(VECTOR_ELT(lst, 3)); + res_code = XGProxyDMatrixSetDataCSR(proxy_dmat, + array_str_indptr.c_str(), + array_str_indices.c_str(), + array_str_data.c_str(), + ncol); + } + CHECK_CALL(res_code); + R_API_END(); + return R_NilValue; +} + +XGB_DLL SEXP XGProxyDMatrixSetDataColumnar_R(SEXP handle, SEXP lst) { + R_API_BEGIN(); + DMatrixHandle proxy_dmat = R_ExternalPtrAddr(handle); + int res_code; + { + std::string sinterface = MakeArrayInterfaceFromRDataFrame(lst); + res_code = XGProxyDMatrixSetDataColumnar(proxy_dmat, sinterface.c_str()); + } + CHECK_CALL(res_code); + R_API_END(); + return R_NilValue; +} + +namespace { + +struct _RDataIterator { + SEXP f_next; + SEXP f_reset; + SEXP calling_env; + SEXP continuation_token; + + _RDataIterator( + SEXP f_next, SEXP f_reset, SEXP calling_env, SEXP continuation_token) : + f_next(f_next), f_reset(f_reset), calling_env(calling_env), + continuation_token(continuation_token) {} + + void reset() { + SafeExecFun(this->f_reset, this->calling_env, this->continuation_token); + } + + int next() { + SEXP R_res = Rf_protect( + SafeExecFun(this->f_next, this->calling_env, this->continuation_token)); + int res = Rf_asInteger(R_res); + Rf_unprotect(1); + return res; + } +}; + +void _reset_RDataIterator(DataIterHandle iter) { + static_cast<_RDataIterator*>(iter)->reset(); +} + +int _next_RDataIterator(DataIterHandle iter) { + return static_cast<_RDataIterator*>(iter)->next(); +} + +SEXP XGDMatrixCreateFromCallbackGeneric_R( + SEXP f_next, SEXP f_reset, SEXP calling_env, SEXP proxy_dmat, + SEXP n_threads, SEXP missing, SEXP max_bin, SEXP ref_dmat, + SEXP cache_prefix, bool as_quantile_dmatrix) { + SEXP continuation_token = Rf_protect(R_MakeUnwindCont()); + SEXP out = Rf_protect(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); + R_API_BEGIN(); + DMatrixHandle out_dmat; + + int res_code; + try { + _RDataIterator data_iterator(f_next, f_reset, calling_env, continuation_token); + + std::string str_cache_prefix; + xgboost::Json jconfig{xgboost::Object{}}; + jconfig["missing"] = Rf_asReal(missing); + if (!Rf_isNull(n_threads)) { + jconfig["nthread"] = Rf_asInteger(n_threads); + } + if (as_quantile_dmatrix) { + if (!Rf_isNull(max_bin)) { + jconfig["max_bin"] = Rf_asInteger(max_bin); + } + } else { + str_cache_prefix = std::string(CHAR(Rf_asChar(cache_prefix))); + jconfig["cache_prefix"] = str_cache_prefix; + } + std::string json_str = xgboost::Json::Dump(jconfig); + + DMatrixHandle ref_dmat_handle = nullptr; + if (as_quantile_dmatrix && !Rf_isNull(ref_dmat)) { + ref_dmat_handle = R_ExternalPtrAddr(ref_dmat); + } + + if (as_quantile_dmatrix) { + res_code = XGQuantileDMatrixCreateFromCallback( + &data_iterator, + R_ExternalPtrAddr(proxy_dmat), + ref_dmat_handle, + _reset_RDataIterator, + _next_RDataIterator, + json_str.c_str(), + &out_dmat); + } else { + res_code = XGDMatrixCreateFromCallback( + &data_iterator, + R_ExternalPtrAddr(proxy_dmat), + _reset_RDataIterator, + _next_RDataIterator, + json_str.c_str(), + &out_dmat); + } + } catch (ErrorWithUnwind &e) { + R_ContinueUnwind(continuation_token); + } + CHECK_CALL(res_code); + + R_SetExternalPtrAddr(out, out_dmat); + R_RegisterCFinalizerEx(out, _DMatrixFinalizer, TRUE); + Rf_unprotect(2); + R_API_END(); + return out; +} + +} /* namespace */ + +XGB_DLL SEXP XGQuantileDMatrixCreateFromCallback_R( + SEXP f_next, SEXP f_reset, SEXP calling_env, SEXP proxy_dmat, + SEXP n_threads, SEXP missing, SEXP max_bin, SEXP ref_dmat) { + return XGDMatrixCreateFromCallbackGeneric_R( + f_next, f_reset, calling_env, proxy_dmat, + n_threads, missing, max_bin, ref_dmat, + R_NilValue, true); +} + +XGB_DLL SEXP XGDMatrixCreateFromCallback_R( + SEXP f_next, SEXP f_reset, SEXP calling_env, SEXP proxy_dmat, + SEXP n_threads, SEXP missing, SEXP cache_prefix) { + return XGDMatrixCreateFromCallbackGeneric_R( + f_next, f_reset, calling_env, proxy_dmat, + n_threads, missing, R_NilValue, R_NilValue, + cache_prefix, false); +} + +XGB_DLL SEXP XGDMatrixFree_R(SEXP proxy_dmat) { + _DMatrixFinalizer(proxy_dmat); + return R_NilValue; +} + +XGB_DLL SEXP XGGetRNAIntAsDouble() { + double sentinel_as_double = static_cast(R_NaInt); + return Rf_ScalarReal(sentinel_as_double); +} + XGB_DLL SEXP XGDuplicate_R(SEXP obj) { return Rf_duplicate(obj); } diff --git a/R-package/src/xgboost_R.h b/R-package/src/xgboost_R.h index ec30dbada..d2e0ae828 100644 --- a/R-package/src/xgboost_R.h +++ b/R-package/src/xgboost_R.h @@ -161,6 +161,84 @@ XGB_DLL SEXP XGDMatrixNumRow_R(SEXP handle); */ XGB_DLL SEXP XGDMatrixNumCol_R(SEXP handle); +/*! +<<<<<<< HEAD + * \brief create a ProxyDMatrix and get an R externalptr object for it + */ +XGB_DLL SEXP XGProxyDMatrixCreate_R(); + +/*! + * \brief Set dense matrix data on a proxy dmatrix + * \param handle R externalptr pointing to a ProxyDMatrix + * \param R_mat R matrix to set in the proxy dmatrix + */ +XGB_DLL SEXP XGProxyDMatrixSetDataDense_R(SEXP handle, SEXP R_mat); + +/*! + * \brief Set dense matrix data on a proxy dmatrix + * \param handle R externalptr pointing to a ProxyDMatrix + * \param lst R list containing, in this order: + * 1. 'p' or 'indptr' vector of the CSR matrix. + * 2. 'j' or 'indices' vector of the CSR matrix. + * 3. 'x' or 'data' vector of the CSR matrix. + * 4. Number of columns in the CSR matrix. + */ +XGB_DLL SEXP XGProxyDMatrixSetDataCSR_R(SEXP handle, SEXP lst); + +/*! + * \brief Set dense matrix data on a proxy dmatrix + * \param handle R externalptr pointing to a ProxyDMatrix + * \param lst R list or data.frame object containing its columns as numeric vectors + */ +XGB_DLL SEXP XGProxyDMatrixSetDataColumnar_R(SEXP handle, SEXP lst); + +/*! + * \brief Create a DMatrix from a DataIter with callbacks + * \param expr_f_next expression for function(env, proxy_dmat) that sets the data on the proxy + * dmatrix and returns either zero (end of batch) or one (batch continues). + * \param expr_f_reset expression for function(env) that resets the data iterator to + * the beginning (first batch). + * \param calling_env R environment where to evaluate the expressions above + * \param proxy_dmat R externalptr holding a ProxyDMatrix. + * \param n_threads number of parallel threads to use for constructing the DMatrix. + * \param missing which value to represent missing value. + * \param cache_prefix path of cache file + * \return handle R externalptr holding the resulting DMatrix. + */ +XGB_DLL SEXP XGDMatrixCreateFromCallback_R( + SEXP expr_f_next, SEXP expr_f_reset, SEXP calling_env, SEXP proxy_dmat, + SEXP n_threads, SEXP missing, SEXP cache_prefix); + +/*! + * \brief Create a QuantileDMatrix from a DataIter with callbacks + * \param expr_f_next expression for function(env, proxy_dmat) that sets the data on the proxy + * dmatrix and returns either zero (end of batch) or one (batch continues). + * \param expr_f_reset expression for function(env) that resets the data iterator to + * the beginning (first batch). + * \param calling_env R environment where to evaluate the expressions above + * \param proxy_dmat R externalptr holding a ProxyDMatrix. + * \param n_threads number of parallel threads to use for constructing the QuantileDMatrix. + * \param missing which value to represent missing value. + * \param max_bin maximum number of bins to have in the resulting QuantileDMatrix. + * \param ref_dmat an optional reference DMatrix from which to get the bin boundaries. + * \return handle R externalptr holding the resulting QuantileDMatrix. + */ +XGB_DLL SEXP XGQuantileDMatrixCreateFromCallback_R( + SEXP expr_f_next, SEXP expr_f_reset, SEXP calling_env, SEXP proxy_dmat, + SEXP n_threads, SEXP missing, SEXP max_bin, SEXP ref_dmat); + +/*! + * \brief Frees a ProxyDMatrix and empties out the R externalptr object that holds it + * \param proxy_dmat R externalptr containing a ProxyDMatrix + * \return NULL + */ +XGB_DLL SEXP XGDMatrixFree_R(SEXP proxy_dmat); + +/*! + * \brief Get the value that represents missingness in R integers as a numeric non-missing value. + */ +XGB_DLL SEXP XGGetRNAIntAsDouble(); + /*! * \brief Call R C-level function 'duplicate' * \param obj Object to duplicate diff --git a/R-package/tests/testthat/test_dmatrix.R b/R-package/tests/testthat/test_dmatrix.R index 568aaa3bd..65374240d 100644 --- a/R-package/tests/testthat/test_dmatrix.R +++ b/R-package/tests/testthat/test_dmatrix.R @@ -343,7 +343,7 @@ test_that("xgb.DMatrix: data.frame", { expect_equal( getinfo(m, "feature_type"), c("float", "float", "int", "i", "c", "c") ) - expect_error(xgb.DMatrix(df)) + expect_error(xgb.DMatrix(df, enable_categorical = FALSE)) df <- data.frame( missing = c("a", "b", "d", NA), @@ -380,6 +380,261 @@ 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: QuantileDMatrix produces same result as DMatrix", { + data(mtcars) + y <- mtcars[, 1] + x <- mtcars[, -1] + + cast_matrix <- function(x) as.matrix(x) + cast_df <- function(x) as.data.frame(x) + cast_csr <- function(x) as(as.matrix(x), "RsparseMatrix") + casting_funs <- list(cast_matrix, cast_df, cast_csr) + + for (casting_fun in casting_funs) { + + qdm <- xgb.QuantileDMatrix( + data = casting_fun(x), + label = y, + nthread = n_threads, + max_bin = 5 + ) + params <- list( + tree_method = "hist", + objective = "reg:squarederror", + nthread = n_threads, + max_bin = 5 + ) + model_qdm <- xgb.train( + params = params, + data = qdm, + nrounds = 2 + ) + pred_qdm <- predict(model_qdm, x) + + dm <- xgb.DMatrix( + data = x, + label = y, + nthread = n_threads + ) + model_dm <- xgb.train( + params = params, + data = dm, + nrounds = 2 + ) + pred_dm <- predict(model_dm, x) + + expect_equal(pred_qdm, pred_dm) + } +}) + +test_that("xgb.DMatrix: QuantileDMatrix is not accepted by exact method", { + data(mtcars) + y <- mtcars[, 1] + x <- as.matrix(mtcars[, -1]) + qdm <- xgb.QuantileDMatrix( + data = x, + label = y, + nthread = n_threads + ) + params <- list( + tree_method = "exact", + objective = "reg:squarederror", + nthread = n_threads + ) + expect_error({ + xgb.train( + params = params, + data = qdm, + nrounds = 2 + ) + }) +}) + +test_that("xgb.DMatrix: ExternalDMatrix produces the same results as regular DMatrix", { + data(mtcars) + y <- mtcars[, 1] + x <- as.matrix(mtcars[, -1]) + set.seed(123) + params <- list( + objective = "reg:squarederror", + nthread = n_threads + ) + model <- xgb.train( + data = xgb.DMatrix(x, label = y), + params = params, + nrounds = 5 + ) + pred <- predict(model, x) + + iterator_env <- as.environment( + list( + iter = 0, + x = mtcars[, -1], + y = mtcars[, 1] + ) + ) + iterator_next <- function(iterator_env, proxy_handle) { + curr_iter <- iterator_env[["iter"]] + if (curr_iter >= 2) { + return(NULL) + } + if (curr_iter == 0) { + x_batch <- iterator_env[["x"]][1:16, ] + y_batch <- iterator_env[["y"]][1:16] + } else { + x_batch <- iterator_env[["x"]][17:32, ] + y_batch <- iterator_env[["y"]][17:32] + } + on.exit({ + iterator_env[["iter"]] <- curr_iter + 1 + }) + return(xgb.ProxyDMatrix(data = x_batch, label = y_batch)) + } + iterator_reset <- function(iterator_env) { + iterator_env[["iter"]] <- 0 + } + data_iterator <- xgb.DataIter( + env = iterator_env, + f_next = iterator_next, + f_reset = iterator_reset + ) + cache_prefix <- tempdir() + edm <- xgb.ExternalDMatrix(data_iterator, cache_prefix, nthread = 1) + expect_true(inherits(edm, "xgb.ExternalDMatrix")) + expect_true(inherits(edm, "xgb.DMatrix")) + set.seed(123) + model_ext <- xgb.train( + data = edm, + params = params, + nrounds = 5 + ) + + pred_model1_edm <- predict(model, edm) + pred_model2_mat <- predict(model_ext, x) + pred_model2_edm <- predict(model_ext, edm) + + expect_equal(pred_model1_edm, pred) + expect_equal(pred_model2_mat, pred) + expect_equal(pred_model2_edm, pred) +}) + +test_that("xgb.DMatrix: External QDM produces same results as regular QDM", { + data(mtcars) + y <- mtcars[, 1] + x <- as.matrix(mtcars[, -1]) + set.seed(123) + params <- list( + objective = "reg:squarederror", + nthread = n_threads, + max_bin = 3 + ) + model <- xgb.train( + data = xgb.QuantileDMatrix( + x, + label = y, + nthread = 1, + max_bin = 3 + ), + params = params, + nrounds = 5 + ) + pred <- predict(model, x) + + iterator_env <- as.environment( + list( + iter = 0, + x = mtcars[, -1], + y = mtcars[, 1] + ) + ) + iterator_next <- function(iterator_env, proxy_handle) { + curr_iter <- iterator_env[["iter"]] + if (curr_iter >= 2) { + return(NULL) + } + if (curr_iter == 0) { + x_batch <- iterator_env[["x"]][1:16, ] + y_batch <- iterator_env[["y"]][1:16] + } else { + x_batch <- iterator_env[["x"]][17:32, ] + y_batch <- iterator_env[["y"]][17:32] + } + on.exit({ + iterator_env[["iter"]] <- curr_iter + 1 + }) + return(xgb.ProxyDMatrix(data = x_batch, label = y_batch)) + } + iterator_reset <- function(iterator_env) { + iterator_env[["iter"]] <- 0 + } + data_iterator <- xgb.DataIter( + env = iterator_env, + f_next = iterator_next, + f_reset = iterator_reset + ) + cache_prefix <- tempdir() + qdm <- xgb.QuantileDMatrix.from_iterator( + data_iterator, + max_bin = 3, + nthread = 1 + ) + expect_true(inherits(qdm, "xgb.QuantileDMatrix")) + expect_true(inherits(qdm, "xgb.DMatrix")) + set.seed(123) + model_ext <- xgb.train( + data = qdm, + params = params, + nrounds = 5 + ) + + pred_model1_qdm <- predict(model, qdm) + pred_model2_mat <- predict(model_ext, x) + pred_model2_qdm <- predict(model_ext, qdm) + + expect_equal(pred_model1_qdm, pred) + expect_equal(pred_model2_mat, pred) + expect_equal(pred_model2_qdm, pred) +}) + +test_that("xgb.DMatrix: R errors thrown on DataIterator are thrown back to the user", { + data(mtcars) + iterator_env <- as.environment( + list( + iter = 0, + x = mtcars[, -1], + y = mtcars[, 1] + ) + ) + iterator_next <- function(iterator_env, proxy_handle) { + curr_iter <- iterator_env[["iter"]] + if (curr_iter >= 2) { + return(0) + } + if (curr_iter == 0) { + x_batch <- iterator_env[["x"]][1:16, ] + y_batch <- iterator_env[["y"]][1:16] + } else { + stop("custom error") + } + on.exit({ + iterator_env[["iter"]] <- curr_iter + 1 + }) + return(xgb.ProxyDMatrix(data = x_batch, label = y_batch)) + } + iterator_reset <- function(iterator_env) { + iterator_env[["iter"]] <- 0 + } + data_iterator <- xgb.DataIter( + env = iterator_env, + f_next = iterator_next, + f_reset = iterator_reset + ) + expect_error( + {xgb.ExternalDMatrix(data_iterator, nthread = 1)}, + "custom error" + ) +}) + test_that("xgb.DMatrix: number of non-missing matches data", { x <- matrix(1:10, nrow = 5) dm1 <- xgb.DMatrix(x) diff --git a/python-package/xgboost/core.py b/python-package/xgboost/core.py index 5761b4b14..27331d3de 100644 --- a/python-package/xgboost/core.py +++ b/python-package/xgboost/core.py @@ -798,9 +798,23 @@ class DMatrix: # pylint: disable=too-many-instance-attributes,too-many-public-m Set names for features. feature_types : - Set types for features. When `enable_categorical` is set to `True`, string - "c" represents categorical data type while "q" represents numerical feature - type. For categorical features, the input is assumed to be preprocessed and + Set types for features. If `data` is a DataFrame type and passing + `enable_categorical=True`, the types will be deduced automatically + from the column types. + + Otherwise, one can pass a list-like input with the same length as number + of columns in `data`, with the following possible values: + - "c", which represents categorical columns. + - "q", which represents numeric columns. + - "int", which represents integer columns. + - "i", which represents boolean columns. + + Note that, while categorical types are treated differently from + the rest for model fitting purposes, the other types do not influence + the generated model, but have effects in other functionalities such as + feature importances. + + For categorical features, the input is assumed to be preprocessed and encoded by the users. The encoding can be done via :py:class:`sklearn.preprocessing.OrdinalEncoder` or pandas dataframe `.cat.codes` method. This is useful when users want to specify categorical From df7cf744b4048e60e11b3b75fd55a74903165ac8 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Tue, 30 Jan 2024 22:17:36 +0100 Subject: [PATCH 09/16] [R] Remove `enable_categorical` parameter (#10018) --- R-package/R/xgb.DMatrix.R | 46 +++++-------------------- R-package/man/xgb.DMatrix.Rd | 24 ++----------- R-package/man/xgb.ProxyDMatrix.Rd | 19 ++-------- R-package/tests/testthat/test_dmatrix.R | 5 ++- 4 files changed, 15 insertions(+), 79 deletions(-) diff --git a/R-package/R/xgb.DMatrix.R b/R-package/R/xgb.DMatrix.R index da036b952..58ba34050 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -16,10 +16,6 @@ #' \item `matrix` objects, with types `numeric`, `integer`, or `logical`. #' \item `data.frame` objects, with columns of types `numeric`, `integer`, `logical`, or `factor`. #' -#' If passing `enable_categorical=TRUE`, columns with `factor` type will be treated as categorical. -#' Otherwise, if passing `enable_categorical=FALSE` and the data contains `factor` columns, an error -#' will be thrown. -#' #' Note that xgboost uses base-0 encoding for categorical types, hence `factor` types (which use base-1 #' encoding') will be converted inside the function call. Be aware that the encoding used for `factor` #' types is not kept as part of the model, so in subsequent calls to `predict`, it is the user's @@ -59,7 +55,7 @@ #' must be the same as in the DMatrix construction, regardless of the column names. #' @param feature_types Set types for features. #' -#' If `data` is a `data.frame` and passing `enable_categorical=TRUE`, the types will be deduced +#' If `data` is a `data.frame` and passing `feature_types` is not supplied, feature types will be deduced #' automatically from the column types. #' #' Otherwise, one can pass a character vector with the same length as number of columns in `data`, @@ -79,18 +75,6 @@ #' @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. #' @return An 'xgb.DMatrix' object. If calling 'xgb.QuantileDMatrix', it will have additional #' subclass 'xgb.QuantileDMatrix'. #' @@ -127,8 +111,7 @@ xgb.DMatrix <- function( qid = NULL, label_lower_bound = NULL, label_upper_bound = NULL, - feature_weights = NULL, - enable_categorical = FALSE + feature_weights = NULL ) { if (!is.null(group) && !is.null(qid)) { stop("Either one of 'group' or 'qid' should be NULL") @@ -180,7 +163,7 @@ xgb.DMatrix <- function( nthread ) } else if (is.data.frame(data)) { - tmp <- .process.df.for.dmatrix(data, enable_categorical, feature_types) + tmp <- .process.df.for.dmatrix(data, feature_types) feature_types <- tmp$feature_types handle <- .Call( XGDMatrixCreateFromDF_R, tmp$lst, missing, nthread @@ -212,7 +195,7 @@ xgb.DMatrix <- function( return(dmat) } -.process.df.for.dmatrix <- function(df, enable_categorical, feature_types) { +.process.df.for.dmatrix <- function(df, feature_types) { if (!nrow(df) || !ncol(df)) { stop("'data' is an empty data.frame.") } @@ -225,12 +208,6 @@ xgb.DMatrix <- function( } else { feature_types <- sapply(df, function(col) { if (is.factor(col)) { - if (!enable_categorical) { - stop( - "When factor type is used, the parameter `enable_categorical`", - " must be set to TRUE." - ) - } return("c") } else if (is.integer(col)) { return("int") @@ -326,7 +303,6 @@ xgb.QuantileDMatrix <- function( label_lower_bound = NULL, label_upper_bound = NULL, feature_weights = NULL, - enable_categorical = FALSE, ref = NULL, max_bin = NULL ) { @@ -357,8 +333,7 @@ xgb.QuantileDMatrix <- function( qid = qid, label_lower_bound = label_lower_bound, label_upper_bound = label_upper_bound, - feature_weights = feature_weights, - enable_categorical = enable_categorical + feature_weights = feature_weights ) ) data_iterator <- .single.data.iterator(iterator_env) @@ -470,8 +445,7 @@ xgb.DataIter <- function(env = new.env(), f_next, f_reset) { qid = env[["qid"]], label_lower_bound = env[["label_lower_bound"]], label_upper_bound = env[["label_upper_bound"]], - feature_weights = env[["feature_weights"]], - enable_categorical = env[["enable_categorical"]] + feature_weights = env[["feature_weights"]] ) ) } @@ -540,8 +514,7 @@ xgb.ProxyDMatrix <- function( qid = NULL, label_lower_bound = NULL, label_upper_bound = NULL, - feature_weights = NULL, - enable_categorical = FALSE + feature_weights = NULL ) { stopifnot(inherits(data, c("matrix", "data.frame", "dgRMatrix"))) out <- list( @@ -555,8 +528,7 @@ xgb.ProxyDMatrix <- function( qid = qid, label_lower_bound = label_lower_bound, label_upper_bound = label_upper_bound, - feature_weights = feature_weights, - enable_categorical = enable_categorical + feature_weights = feature_weights ) class(out) <- "xgb.ProxyDMatrix" return(out) @@ -575,7 +547,7 @@ xgb.ProxyDMatrix.internal <- function(proxy_handle, data_iterator) { stop("Either one of 'group' or 'qid' should be NULL") } if (is.data.frame(lst$data)) { - tmp <- .process.df.for.dmatrix(lst$data, lst$enable_categorical, lst$feature_types) + tmp <- .process.df.for.dmatrix(lst$data, lst$feature_types) lst$feature_types <- tmp$feature_types .Call(XGProxyDMatrixSetDataColumnar_R, proxy_handle, tmp$lst) rm(tmp) diff --git a/R-package/man/xgb.DMatrix.Rd b/R-package/man/xgb.DMatrix.Rd index ceb60dc42..8e108a2fa 100644 --- a/R-package/man/xgb.DMatrix.Rd +++ b/R-package/man/xgb.DMatrix.Rd @@ -19,8 +19,7 @@ xgb.DMatrix( qid = NULL, label_lower_bound = NULL, label_upper_bound = NULL, - feature_weights = NULL, - enable_categorical = FALSE + feature_weights = NULL ) xgb.QuantileDMatrix( @@ -37,7 +36,6 @@ xgb.QuantileDMatrix( label_lower_bound = NULL, label_upper_bound = NULL, feature_weights = NULL, - enable_categorical = FALSE, ref = NULL, max_bin = NULL ) @@ -50,10 +48,6 @@ Supported input types are as follows:\itemize{ \item \code{matrix} objects, with types \code{numeric}, \code{integer}, or \code{logical}. \item \code{data.frame} objects, with columns of types \code{numeric}, \code{integer}, \code{logical}, or \code{factor}. -If passing \code{enable_categorical=TRUE}, columns with \code{factor} type will be treated as categorical. -Otherwise, if passing \code{enable_categorical=FALSE} and the data contains \code{factor} columns, an error -will be thrown. - Note that xgboost uses base-0 encoding for categorical types, hence \code{factor} types (which use base-1 encoding') will be converted inside the function call. Be aware that the encoding used for \code{factor} types is not kept as part of the model, so in subsequent calls to \code{predict}, it is the user's @@ -102,7 +96,7 @@ frame and matrix. \item{feature_types}{Set types for features. -If \code{data} is a \code{data.frame} and passing \code{enable_categorical=TRUE}, the types will be deduced +If \code{data} is a \code{data.frame} and passing \code{feature_types} is not supplied, feature types will be deduced automatically from the column types. Otherwise, one can pass a character vector with the same length as number of columns in \code{data}, @@ -129,20 +123,6 @@ functionalities such as feature importances.} \item{feature_weights}{Set feature weights for column sampling.} -\item{enable_categorical}{Experimental support of specializing for categorical features. - -\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 'data' is not a data frame, this argument is ignored. - - JSON/UBJSON serialization format is required for this. -}\if{html}{\out{
}}} - \item{ref}{The training dataset that provides quantile information, needed when creating validation/test dataset with \code{xgb.QuantileDMatrix}. Supplying the training DMatrix as a reference means that the same quantisation applied to the training data is diff --git a/R-package/man/xgb.ProxyDMatrix.Rd b/R-package/man/xgb.ProxyDMatrix.Rd index 5a9b6251a..cf173a2db 100644 --- a/R-package/man/xgb.ProxyDMatrix.Rd +++ b/R-package/man/xgb.ProxyDMatrix.Rd @@ -15,8 +15,7 @@ xgb.ProxyDMatrix( qid = NULL, label_lower_bound = NULL, label_upper_bound = NULL, - feature_weights = NULL, - enable_categorical = FALSE + feature_weights = NULL ) } \arguments{ @@ -57,7 +56,7 @@ frame and matrix. \item{feature_types}{Set types for features. -If \code{data} is a \code{data.frame} and passing \code{enable_categorical=TRUE}, the types will be deduced +If \code{data} is a \code{data.frame} and passing \code{feature_types} is not supplied, feature types will be deduced automatically from the column types. Otherwise, one can pass a character vector with the same length as number of columns in \code{data}, @@ -81,20 +80,6 @@ functionalities such as feature importances.} \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. - -\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 'data' is not a data frame, this argument is ignored. - - JSON/UBJSON serialization format is required for this. -}\if{html}{\out{
}}} } \value{ An object of class \code{xgb.ProxyDMatrix}, which is just a list containing the diff --git a/R-package/tests/testthat/test_dmatrix.R b/R-package/tests/testthat/test_dmatrix.R index 65374240d..cd871c8c8 100644 --- a/R-package/tests/testthat/test_dmatrix.R +++ b/R-package/tests/testthat/test_dmatrix.R @@ -338,19 +338,18 @@ test_that("xgb.DMatrix: data.frame", { stringsAsFactors = TRUE ) - m <- xgb.DMatrix(df, enable_categorical = TRUE) + m <- xgb.DMatrix(df) expect_equal(colnames(m), colnames(df)) expect_equal( getinfo(m, "feature_type"), c("float", "float", "int", "i", "c", "c") ) - expect_error(xgb.DMatrix(df, enable_categorical = FALSE)) df <- data.frame( missing = c("a", "b", "d", NA), valid = c("a", "b", "d", "c"), stringsAsFactors = TRUE ) - m <- xgb.DMatrix(df, enable_categorical = TRUE) + m <- xgb.DMatrix(df) expect_equal(getinfo(m, "feature_type"), c("c", "c")) }) From 5e00a71671d0e57a76e754b1b15c4619473d532a Mon Sep 17 00:00:00 2001 From: david-cortes Date: Tue, 30 Jan 2024 22:33:37 +0100 Subject: [PATCH 10/16] [R] rename slice to avoid dplyr conflict (#10017) --- R-package/NAMESPACE | 3 +-- R-package/R/xgb.DMatrix.R | 14 +++++--------- R-package/R/xgb.cv.R | 6 +++--- .../{slice.xgb.DMatrix.Rd => xgb.slice.DMatrix.Rd} | 11 ++++------- R-package/tests/testthat/test_dmatrix.R | 6 +++--- 5 files changed, 16 insertions(+), 24 deletions(-) rename R-package/man/{slice.xgb.DMatrix.Rd => xgb.slice.DMatrix.Rd} (84%) diff --git a/R-package/NAMESPACE b/R-package/NAMESPACE index 49f93bb57..bb5959eab 100644 --- a/R-package/NAMESPACE +++ b/R-package/NAMESPACE @@ -15,7 +15,6 @@ 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<-") @@ -30,7 +29,6 @@ export(cb.reset.parameters) export(cb.save.model) export(getinfo) export(setinfo) -export(slice) export(xgb.DMatrix) export(xgb.DMatrix.hasinfo) export(xgb.DMatrix.save) @@ -70,6 +68,7 @@ export(xgb.save) export(xgb.save.raw) export(xgb.set.config) export(xgb.slice.Booster) +export(xgb.slice.DMatrix) export(xgb.train) export(xgboost) import(methods) diff --git a/R-package/R/xgb.DMatrix.R b/R-package/R/xgb.DMatrix.R index 58ba34050..ad446d248 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -1228,19 +1228,15 @@ xgb.get.DMatrix.data <- function(dmat) { #' data(agaricus.train, package='xgboost') #' dtrain <- with(agaricus.train, xgb.DMatrix(data, label = label, nthread = 2)) #' -#' dsub <- slice(dtrain, 1:42) +#' dsub <- xgb.slice.DMatrix(dtrain, 1:42) #' labels1 <- getinfo(dsub, 'label') #' dsub <- dtrain[1:42, ] #' labels2 <- getinfo(dsub, 'label') #' all.equal(labels1, labels2) #' -#' @rdname slice.xgb.DMatrix +#' @rdname xgb.slice.DMatrix #' @export -slice <- function(object, idxset) UseMethod("slice") - -#' @rdname slice.xgb.DMatrix -#' @export -slice.xgb.DMatrix <- function(object, idxset) { +xgb.slice.DMatrix <- function(object, idxset) { if (!inherits(object, "xgb.DMatrix")) { stop("object must be xgb.DMatrix") } @@ -1264,10 +1260,10 @@ slice.xgb.DMatrix <- function(object, idxset) { return(structure(ret, class = "xgb.DMatrix")) } -#' @rdname slice.xgb.DMatrix +#' @rdname xgb.slice.DMatrix #' @export `[.xgb.DMatrix` <- function(object, idxset, colset = NULL) { - slice(object, idxset) + xgb.slice.DMatrix(object, idxset) } diff --git a/R-package/R/xgb.cv.R b/R-package/R/xgb.cv.R index eb0495631..29bddb57f 100644 --- a/R-package/R/xgb.cv.R +++ b/R-package/R/xgb.cv.R @@ -197,12 +197,12 @@ xgb.cv <- function(params = list(), data, nrounds, nfold, label = NULL, missing nthread = params$nthread ) bst_folds <- lapply(seq_along(folds), function(k) { - dtest <- slice(dall, folds[[k]]) + dtest <- xgb.slice.DMatrix(dall, folds[[k]]) # code originally contributed by @RolandASc on stackoverflow if (is.null(train_folds)) - dtrain <- slice(dall, unlist(folds[-k])) + dtrain <- xgb.slice.DMatrix(dall, unlist(folds[-k])) else - dtrain <- slice(dall, train_folds[[k]]) + dtrain <- xgb.slice.DMatrix(dall, train_folds[[k]]) bst <- xgb.Booster( params = params, cachelist = list(dtrain, dtest), diff --git a/R-package/man/slice.xgb.DMatrix.Rd b/R-package/man/xgb.slice.DMatrix.Rd similarity index 84% rename from R-package/man/slice.xgb.DMatrix.Rd rename to R-package/man/xgb.slice.DMatrix.Rd index a2dfb699b..c9695996b 100644 --- a/R-package/man/slice.xgb.DMatrix.Rd +++ b/R-package/man/xgb.slice.DMatrix.Rd @@ -1,15 +1,12 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/xgb.DMatrix.R -\name{slice} -\alias{slice} -\alias{slice.xgb.DMatrix} +\name{xgb.slice.DMatrix} +\alias{xgb.slice.DMatrix} \alias{[.xgb.DMatrix} \title{Get a new DMatrix containing the specified rows of original xgb.DMatrix object} \usage{ -slice(object, idxset) - -\method{slice}{xgb.DMatrix}(object, idxset) +xgb.slice.DMatrix(object, idxset) \method{[}{xgb.DMatrix}(object, idxset, colset = NULL) } @@ -28,7 +25,7 @@ original xgb.DMatrix object data(agaricus.train, package='xgboost') dtrain <- with(agaricus.train, xgb.DMatrix(data, label = label, nthread = 2)) -dsub <- slice(dtrain, 1:42) +dsub <- xgb.slice.DMatrix(dtrain, 1:42) labels1 <- getinfo(dsub, 'label') dsub <- dtrain[1:42, ] labels2 <- getinfo(dsub, 'label') diff --git a/R-package/tests/testthat/test_dmatrix.R b/R-package/tests/testthat/test_dmatrix.R index cd871c8c8..43b7515bb 100644 --- a/R-package/tests/testthat/test_dmatrix.R +++ b/R-package/tests/testthat/test_dmatrix.R @@ -166,7 +166,7 @@ test_that("xgb.DMatrix: getinfo & setinfo", { test_that("xgb.DMatrix: slice, dim", { dtest <- xgb.DMatrix(test_data, label = test_label, nthread = n_threads) expect_equal(dim(dtest), dim(test_data)) - dsub1 <- slice(dtest, 1:42) + dsub1 <- xgb.slice.DMatrix(dtest, 1:42) expect_equal(nrow(dsub1), 42) expect_equal(ncol(dsub1), ncol(test_data)) @@ -182,12 +182,12 @@ test_that("xgb.DMatrix: slice, trailing empty rows", { dtrain <- xgb.DMatrix( data = train_data, label = train_label, nthread = n_threads ) - slice(dtrain, 6513L) + xgb.slice.DMatrix(dtrain, 6513L) train_data[6513, ] <- 0 dtrain <- xgb.DMatrix( data = train_data, label = train_label, nthread = n_threads ) - slice(dtrain, 6513L) + xgb.slice.DMatrix(dtrain, 6513L) expect_equal(nrow(dtrain), 6513) }) From c53d59f8db69bf97861ed5384e6f77e17e423518 Mon Sep 17 00:00:00 2001 From: Ammar Azman <88831990+Ammar-Azman@users.noreply.github.com> Date: Wed, 31 Jan 2024 15:18:16 +0800 Subject: [PATCH 11/16] [doc] Fix typos in code (#10023) change `resutls` to `results` --- doc/python/sklearn_estimator.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/python/sklearn_estimator.rst b/doc/python/sklearn_estimator.rst index a4835dcac..207b9fa30 100644 --- a/doc/python/sklearn_estimator.rst +++ b/doc/python/sklearn_estimator.rst @@ -104,7 +104,7 @@ using cross validation with early stopping, here is a snippet to begin with: clf = xgb.XGBClassifier(tree_method="hist", early_stopping_rounds=3) - resutls = {} + results = {} for train, test in cv.split(X, y): X_train = X[train] @@ -114,7 +114,7 @@ using cross validation with early stopping, here is a snippet to begin with: est, train_score, test_score = fit_and_score( clone(clf), X_train, X_test, y_train, y_test ) - resutls[est] = (train_score, test_score) + results[est] = (train_score, test_score) *********************************** From 1e72dc12761a753caffd59ab51f201f859f15834 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Wed, 31 Jan 2024 08:43:22 +0100 Subject: [PATCH 12/16] [R] Add optional check on column names matching in `predict` (#10020) --- R-package/R/xgb.Booster.R | 100 +++++++++++++++++++++++- R-package/man/predict.xgb.Booster.Rd | 18 +++++ R-package/tests/testthat/test_helpers.R | 79 +++++++++++++++++++ 3 files changed, 196 insertions(+), 1 deletion(-) diff --git a/R-package/R/xgb.Booster.R b/R-package/R/xgb.Booster.R index 7613c9152..febefb757 100644 --- a/R-package/R/xgb.Booster.R +++ b/R-package/R/xgb.Booster.R @@ -111,6 +111,21 @@ xgb.get.handle <- function(object) { #' 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 validate_features When `TRUE`, validate that the Booster's and newdata's feature_names +#' match (only applicable when both `object` and `newdata` have feature names). +#' +#' If the column names differ and `newdata` is not an `xgb.DMatrix`, will try to reorder +#' the columns in `newdata` to match with the booster's. +#' +#' If the booster has feature types and `newdata` is either an `xgb.DMatrix` or `data.frame`, +#' will additionally verify that categorical columns are of the correct type in `newdata`, +#' throwing an error if they do not match. +#' +#' If passing `FALSE`, it is assumed that the feature names and types are the same, +#' and come in the same order as in the training data. +#' +#' Note that this check might add some sizable latency to the predictions, so it's +#' recommended to disable it for performance-sensitive applications. #' @param ... Not used. #' #' @details @@ -271,7 +286,11 @@ xgb.get.handle <- function(object) { #' @export 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, ...) { + reshape = FALSE, training = FALSE, iterationrange = NULL, strict_shape = FALSE, + validate_features = FALSE, ...) { + if (validate_features) { + newdata <- validate.features(object, newdata) + } if (!inherits(newdata, "xgb.DMatrix")) { nthread <- xgb.nthread(object) newdata <- xgb.DMatrix( @@ -418,6 +437,85 @@ predict.xgb.Booster <- function(object, newdata, missing = NA, outputmargin = FA return(arr) } +validate.features <- function(bst, newdata) { + if (is.character(newdata)) { + # this will be encountered when passing file paths + return(newdata) + } + if (inherits(newdata, "sparseVector")) { + # in this case, newdata won't have metadata + return(newdata) + } + if (is.vector(newdata)) { + newdata <- as.matrix(newdata) + } + + booster_names <- getinfo(bst, "feature_name") + checked_names <- FALSE + if (NROW(booster_names)) { + + try_reorder <- FALSE + if (inherits(newdata, "xgb.DMatrix")) { + curr_names <- getinfo(newdata, "feature_name") + } else { + curr_names <- colnames(newdata) + try_reorder <- TRUE + } + + if (NROW(curr_names)) { + checked_names <- TRUE + + if (length(curr_names) != length(booster_names) || any(curr_names != booster_names)) { + + if (!try_reorder) { + stop("Feature names in 'newdata' do not match with booster's.") + } else { + if (inherits(newdata, "data.table")) { + newdata <- newdata[, booster_names, with = FALSE] + } else { + newdata <- newdata[, booster_names, drop = FALSE] + } + } + + } + + } # if (NROW(curr_names)) { + + } # if (NROW(booster_names)) { + + if (inherits(newdata, c("data.frame", "xgb.DMatrix"))) { + + booster_types <- getinfo(bst, "feature_type") + if (!NROW(booster_types)) { + # Note: types in the booster are optional. Other interfaces + # might not even save it as booster attributes for example, + # even if the model uses categorical features. + return(newdata) + } + if (inherits(newdata, "xgb.DMatrix")) { + curr_types <- getinfo(newdata, "feature_type") + if (length(curr_types) != length(booster_types) || any(curr_types != booster_types)) { + stop("Feature types in 'newdata' do not match with booster's.") + } + } + if (inherits(newdata, "data.frame")) { + is_factor <- sapply(newdata, is.factor) + if (any(is_factor != (booster_types == "c"))) { + stop( + paste0( + "Feature types in 'newdata' do not match with booster's for same columns (by ", + ifelse(checked_names, "name", "position"), + ")." + ) + ) + } + } + + } + + return(newdata) +} + #' @title Accessors for serializable attributes of a model #' diff --git a/R-package/man/predict.xgb.Booster.Rd b/R-package/man/predict.xgb.Booster.Rd index 7a6dd6c13..95e7a51fd 100644 --- a/R-package/man/predict.xgb.Booster.Rd +++ b/R-package/man/predict.xgb.Booster.Rd @@ -17,6 +17,7 @@ training = FALSE, iterationrange = NULL, strict_shape = FALSE, + validate_features = FALSE, ... ) } @@ -66,6 +67,23 @@ base-1 indexing, and inclusive of both ends). \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{validate_features}{When \code{TRUE}, validate that the Booster's and newdata's feature_names +match (only applicable when both \code{object} and \code{newdata} have feature names). + +\if{html}{\out{
}}\preformatted{ If the column names differ and `newdata` is not an `xgb.DMatrix`, will try to reorder + the columns in `newdata` to match with the booster's. + + If the booster has feature types and `newdata` is either an `xgb.DMatrix` or `data.frame`, + will additionally verify that categorical columns are of the correct type in `newdata`, + throwing an error if they do not match. + + If passing `FALSE`, it is assumed that the feature names and types are the same, + and come in the same order as in the training data. + + Note that this check might add some sizable latency to the predictions, so it's + recommended to disable it for performance-sensitive applications. +}\if{html}{\out{
}}} + \item{...}{Not used.} } \value{ diff --git a/R-package/tests/testthat/test_helpers.R b/R-package/tests/testthat/test_helpers.R index badac0213..38b5ca066 100644 --- a/R-package/tests/testthat/test_helpers.R +++ b/R-package/tests/testthat/test_helpers.R @@ -511,3 +511,82 @@ test_that('convert.labels works', { expect_equal(class(res), 'numeric') } }) + +test_that("validate.features works as expected", { + data(mtcars) + y <- mtcars$mpg + x <- as.matrix(mtcars[, -1]) + dm <- xgb.DMatrix(x, label = y, nthread = 1) + model <- xgb.train( + params = list(nthread = 1), + data = dm, + nrounds = 3 + ) + + # result is output as-is when needed + res <- validate.features(model, x) + expect_equal(res, x) + res <- validate.features(model, dm) + expect_identical(res, dm) + res <- validate.features(model, as(x[1, ], "dsparseVector")) + expect_equal(as.numeric(res), unname(x[1, ])) + res <- validate.features(model, "file.txt") + expect_equal(res, "file.txt") + + # columns are reordered + res <- validate.features(model, mtcars[, rev(names(mtcars))]) + expect_equal(names(res), colnames(x)) + expect_equal(as.matrix(res), x) + res <- validate.features(model, as.matrix(mtcars[, rev(names(mtcars))])) + expect_equal(colnames(res), colnames(x)) + expect_equal(res, x) + res <- validate.features(model, mtcars[1, rev(names(mtcars)), drop = FALSE]) + expect_equal(names(res), colnames(x)) + expect_equal(unname(as.matrix(res)), unname(x[1, , drop = FALSE])) + res <- validate.features(model, as.data.table(mtcars[, rev(names(mtcars))])) + expect_equal(names(res), colnames(x)) + expect_equal(unname(as.matrix(res)), unname(x)) + + # error when columns are missing + expect_error({ + validate.features(model, mtcars[, 1:3]) + }) + expect_error({ + validate.features(model, as.matrix(mtcars[, 1:ncol(x)])) # nolint + }) + expect_error({ + validate.features(model, xgb.DMatrix(mtcars[, 1:3])) + }) + expect_error({ + validate.features(model, as(x[, 1:3], "CsparseMatrix")) + }) + + # error when it cannot reorder or subset + expect_error({ + validate.features(model, xgb.DMatrix(mtcars)) + }, "Feature names") + expect_error({ + validate.features(model, xgb.DMatrix(x[, rev(colnames(x))])) + }, "Feature names") + + # no error about types if the booster doesn't have types + expect_error({ + validate.features(model, xgb.DMatrix(x, feature_types = c(rep("q", 5), rep("c", 5)))) + }, NA) + tmp <- mtcars + tmp[["vs"]] <- factor(tmp[["vs"]]) + expect_error({ + validate.features(model, tmp) + }, NA) + + # error when types do not match + setinfo(model, "feature_type", rep("q", 10)) + expect_error({ + validate.features(model, xgb.DMatrix(x, feature_types = c(rep("q", 5), rep("c", 5)))) + }, "Feature types") + tmp <- mtcars + tmp[["vs"]] <- factor(tmp[["vs"]]) + expect_error({ + validate.features(model, tmp) + }, "Feature types") +}) From 09552132209541d8f373f003ebdd4b734cb2f84d Mon Sep 17 00:00:00 2001 From: david-cortes Date: Wed, 31 Jan 2024 08:43:58 +0100 Subject: [PATCH 13/16] [R] rename proxy dmatrix -> data batch (#10016) --- R-package/NAMESPACE | 2 +- R-package/R/xgb.DMatrix.R | 43 ++++++++++--------- .../{xgb.ProxyDMatrix.Rd => xgb.DataBatch.Rd} | 14 +++--- R-package/man/xgb.DataIter.Rd | 6 +-- R-package/man/xgb.ExternalDMatrix.Rd | 6 +-- .../man/xgb.QuantileDMatrix.from_iterator.Rd | 2 +- R-package/tests/testthat/test_dmatrix.R | 12 +++--- 7 files changed, 43 insertions(+), 42 deletions(-) rename R-package/man/{xgb.ProxyDMatrix.Rd => xgb.DataBatch.Rd} (93%) diff --git a/R-package/NAMESPACE b/R-package/NAMESPACE index bb5959eab..580d1f873 100644 --- a/R-package/NAMESPACE +++ b/R-package/NAMESPACE @@ -32,9 +32,9 @@ export(setinfo) export(xgb.DMatrix) export(xgb.DMatrix.hasinfo) export(xgb.DMatrix.save) +export(xgb.DataBatch) export(xgb.DataIter) export(xgb.ExternalDMatrix) -export(xgb.ProxyDMatrix) export(xgb.QuantileDMatrix) export(xgb.QuantileDMatrix.from_iterator) export(xgb.attr) diff --git a/R-package/R/xgb.DMatrix.R b/R-package/R/xgb.DMatrix.R index ad446d248..a4c476dbc 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -348,7 +348,7 @@ xgb.QuantileDMatrix <- function( .Call(XGDMatrixFree_R, proxy_handle) }) iterator_next <- function() { - return(xgb.ProxyDMatrix.internal(proxy_handle, data_iterator)) + return(xgb.ProxyDMatrix(proxy_handle, data_iterator)) } iterator_reset <- function() { return(data_iterator$f_reset(iterator_env)) @@ -391,12 +391,12 @@ xgb.QuantileDMatrix <- function( #' to know which part of the data to pass next. #' @param f_next `function(env)` which is responsible for:\itemize{ #' \item Accessing or retrieving the next batch of data in the iterator. -#' \item Supplying this data by calling function \link{xgb.ProxyDMatrix} on it and returning the result. +#' \item Supplying this data by calling function \link{xgb.DataBatch} on it and returning the result. #' \item Keeping track of where in the iterator batch it is or will go next, which can for example #' be done by modifiying variables in the `env` variable that is passed here. #' \item Signaling whether there are more batches to be consumed or not, by returning `NULL` #' when the stream of data ends (all batches in the iterator have been consumed), or the result from -#' calling \link{xgb.ProxyDMatrix} when there are more batches in the line to be consumed. +#' calling \link{xgb.DataBatch} when there are more batches in the line to be consumed. #' } #' @param f_reset `function(env)` which is responsible for reseting the data iterator #' (i.e. taking it back to the first batch, called before and after the sequence of batches @@ -406,7 +406,7 @@ xgb.QuantileDMatrix <- function( #' (and in the same order) must be passed in subsequent iterations. #' @return An `xgb.DataIter` object, containing the same inputs supplied here, which can then #' be passed to \link{xgb.ExternalDMatrix}. -#' @seealso \link{xgb.ExternalDMatrix}, \link{xgb.ProxyDMatrix}. +#' @seealso \link{xgb.ExternalDMatrix}, \link{xgb.DataBatch}. #' @export xgb.DataIter <- function(env = new.env(), f_next, f_reset) { if (!is.function(f_next)) { @@ -434,7 +434,7 @@ xgb.DataIter <- function(env = new.env(), f_next, f_reset) { env[["iter"]] <- curr_iter + 1L }) return( - xgb.ProxyDMatrix( + xgb.DataBatch( data = env[["data"]], label = env[["label"]], weight = env[["weight"]], @@ -464,13 +464,13 @@ xgb.DataIter <- function(env = new.env(), f_next, f_reset) { .make.proxy.handle <- function() { out <- .Call(XGProxyDMatrixCreate_R) attributes(out) <- list( - class = c("xgb.DMatrix", "xgb.ProxyDMatrixHandle"), + class = c("xgb.DMatrix", "xgb.ProxyDMatrix"), fields = new.env() ) return(out) } -#' @title Proxy DMatrix Updater +#' @title Structure for Data Batches #' @description Helper function to supply data in batches of a data iterator when #' constructing a DMatrix from external memory through \link{xgb.ExternalDMatrix} #' or through \link{xgb.QuantileDMatrix.from_iterator}. @@ -480,8 +480,8 @@ xgb.DataIter <- function(env = new.env(), f_next, f_reset) { #' when constructing a DMatrix through external memory - otherwise, one should call #' \link{xgb.DMatrix} or \link{xgb.QuantileDMatrix}. #' -#' The object that results from calling this function directly is \bold{not} like the other -#' `xgb.DMatrix` variants - i.e. cannot be used to train a model, nor to get predictions - only +#' The object that results from calling this function directly is \bold{not} like +#' an `xgb.DMatrix` - i.e. cannot be used to train a model, nor to get predictions - only #' possible usage is to supply data to an iterator, from which a DMatrix is then constructed. #' #' For more information and for example usage, see the documentation for \link{xgb.ExternalDMatrix}. @@ -499,11 +499,11 @@ xgb.DataIter <- function(env = new.env(), f_next, f_reset) { #' \link{xgb.DMatrix} for details on it. #' \item CSR matrices, as class `dgRMatrix` from package `Matrix`. #' } -#' @return An object of class `xgb.ProxyDMatrix`, which is just a list containing the +#' @return An object of class `xgb.DataBatch`, which is just a list containing the #' data and parameters passed here. It does \bold{not} inherit from `xgb.DMatrix`. #' @seealso \link{xgb.DataIter}, \link{xgb.ExternalDMatrix}. #' @export -xgb.ProxyDMatrix <- function( +xgb.DataBatch <- function( data, label = NULL, weight = NULL, @@ -530,17 +530,18 @@ xgb.ProxyDMatrix <- function( label_upper_bound = label_upper_bound, feature_weights = feature_weights ) - class(out) <- "xgb.ProxyDMatrix" + class(out) <- "xgb.DataBatch" return(out) } -xgb.ProxyDMatrix.internal <- function(proxy_handle, data_iterator) { +# This is only for internal usage, class is not exposed to the user. +xgb.ProxyDMatrix <- function(proxy_handle, data_iterator) { lst <- data_iterator$f_next(data_iterator$env) if (is.null(lst)) { return(0L) } - if (!inherits(lst, "xgb.ProxyDMatrix")) { - stop("DataIter 'f_next' must return either NULL or the result from calling 'xgb.ProxyDMatrix'.") + if (!inherits(lst, "xgb.DataBatch")) { + stop("DataIter 'f_next' must return either NULL or the result from calling 'xgb.DataBatch'.") } if (!is.null(lst$group) && !is.null(lst$qid)) { @@ -606,7 +607,7 @@ xgb.ProxyDMatrix.internal <- function(proxy_handle, data_iterator) { #' This should not pose any problem for `numeric` types, since they do have an inheret NaN value. #' @return An 'xgb.DMatrix' object, with subclass 'xgb.ExternalDMatrix', in which the data is not #' held internally but accessed through the iterator when needed. -#' @seealso \link{xgb.DataIter}, \link{xgb.ProxyDMatrix}, \link{xgb.QuantileDMatrix.from_iterator} +#' @seealso \link{xgb.DataIter}, \link{xgb.DataBatch}, \link{xgb.QuantileDMatrix.from_iterator} #' @examples #' library(xgboost) #' data(mtcars) @@ -646,10 +647,10 @@ xgb.ProxyDMatrix.internal <- function(proxy_handle, data_iterator) { #' iterator_env[["iter"]] <- curr_iter + 1 #' }) #' -#' # Function 'xgb.ProxyDMatrix' must be called manually +#' # Function 'xgb.DataBatch' must be called manually #' # at each batch with all the appropriate attributes, #' # such as feature names and feature types. -#' return(xgb.ProxyDMatrix(data = x_batch, label = y_batch)) +#' return(xgb.DataBatch(data = x_batch, label = y_batch)) #' } #' #' # This moves the iterator back to its beginning @@ -693,7 +694,7 @@ xgb.ExternalDMatrix <- function( .Call(XGDMatrixFree_R, proxy_handle) }) iterator_next <- function() { - return(xgb.ProxyDMatrix.internal(proxy_handle, data_iterator)) + return(xgb.ProxyDMatrix(proxy_handle, data_iterator)) } iterator_reset <- function() { return(data_iterator$f_reset(data_iterator$env)) @@ -736,7 +737,7 @@ xgb.ExternalDMatrix <- function( #' @inheritParams xgb.ExternalDMatrix #' @inheritParams xgb.QuantileDMatrix #' @return An 'xgb.DMatrix' object, with subclass 'xgb.QuantileDMatrix'. -#' @seealso \link{xgb.DataIter}, \link{xgb.ProxyDMatrix}, \link{xgb.ExternalDMatrix}, +#' @seealso \link{xgb.DataIter}, \link{xgb.DataBatch}, \link{xgb.ExternalDMatrix}, #' \link{xgb.QuantileDMatrix} #' @export xgb.QuantileDMatrix.from_iterator <- function( # nolint @@ -758,7 +759,7 @@ xgb.QuantileDMatrix.from_iterator <- function( # nolint .Call(XGDMatrixFree_R, proxy_handle) }) iterator_next <- function() { - return(xgb.ProxyDMatrix.internal(proxy_handle, data_iterator)) + return(xgb.ProxyDMatrix(proxy_handle, data_iterator)) } iterator_reset <- function() { return(data_iterator$f_reset(data_iterator$env)) diff --git a/R-package/man/xgb.ProxyDMatrix.Rd b/R-package/man/xgb.DataBatch.Rd similarity index 93% rename from R-package/man/xgb.ProxyDMatrix.Rd rename to R-package/man/xgb.DataBatch.Rd index cf173a2db..0eeb117e8 100644 --- a/R-package/man/xgb.ProxyDMatrix.Rd +++ b/R-package/man/xgb.DataBatch.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/xgb.DMatrix.R -\name{xgb.ProxyDMatrix} -\alias{xgb.ProxyDMatrix} -\title{Proxy DMatrix Updater} +\name{xgb.DataBatch} +\alias{xgb.DataBatch} +\title{Structure for Data Batches} \usage{ -xgb.ProxyDMatrix( +xgb.DataBatch( data, label = NULL, weight = NULL, @@ -82,7 +82,7 @@ functionalities such as feature importances.} \item{feature_weights}{Set feature weights for column sampling.} } \value{ -An object of class \code{xgb.ProxyDMatrix}, which is just a list containing the +An object of class \code{xgb.DataBatch}, which is just a list containing the data and parameters passed here. It does \bold{not} inherit from \code{xgb.DMatrix}. } \description{ @@ -95,8 +95,8 @@ is passed as argument to function \link{xgb.DataIter} to construct a data iterat when constructing a DMatrix through external memory - otherwise, one should call \link{xgb.DMatrix} or \link{xgb.QuantileDMatrix}. -The object that results from calling this function directly is \bold{not} like the other -\code{xgb.DMatrix} variants - i.e. cannot be used to train a model, nor to get predictions - only +The object that results from calling this function directly is \bold{not} like +an \code{xgb.DMatrix} - i.e. cannot be used to train a model, nor to get predictions - only possible usage is to supply data to an iterator, from which a DMatrix is then constructed. For more information and for example usage, see the documentation for \link{xgb.ExternalDMatrix}. diff --git a/R-package/man/xgb.DataIter.Rd b/R-package/man/xgb.DataIter.Rd index 29cf5acc9..2bd68ce51 100644 --- a/R-package/man/xgb.DataIter.Rd +++ b/R-package/man/xgb.DataIter.Rd @@ -15,12 +15,12 @@ to know which part of the data to pass next.} \item{f_next}{\verb{function(env)} which is responsible for:\itemize{ \item Accessing or retrieving the next batch of data in the iterator. -\item Supplying this data by calling function \link{xgb.ProxyDMatrix} on it and returning the result. +\item Supplying this data by calling function \link{xgb.DataBatch} on it and returning the result. \item Keeping track of where in the iterator batch it is or will go next, which can for example be done by modifiying variables in the \code{env} variable that is passed here. \item Signaling whether there are more batches to be consumed or not, by returning \code{NULL} when the stream of data ends (all batches in the iterator have been consumed), or the result from -calling \link{xgb.ProxyDMatrix} when there are more batches in the line to be consumed. +calling \link{xgb.DataBatch} when there are more batches in the line to be consumed. }} \item{f_reset}{\verb{function(env)} which is responsible for reseting the data iterator @@ -47,5 +47,5 @@ which will consume the data and create a DMatrix from it by executing the callba For more information, and for a usage example, see the documentation for \link{xgb.ExternalDMatrix}. } \seealso{ -\link{xgb.ExternalDMatrix}, \link{xgb.ProxyDMatrix}. +\link{xgb.ExternalDMatrix}, \link{xgb.DataBatch}. } diff --git a/R-package/man/xgb.ExternalDMatrix.Rd b/R-package/man/xgb.ExternalDMatrix.Rd index 3e7844990..14a872cb5 100644 --- a/R-package/man/xgb.ExternalDMatrix.Rd +++ b/R-package/man/xgb.ExternalDMatrix.Rd @@ -87,10 +87,10 @@ iterator_next <- function(iterator_env) { iterator_env[["iter"]] <- curr_iter + 1 }) - # Function 'xgb.ProxyDMatrix' must be called manually + # Function 'xgb.DataBatch' must be called manually # at each batch with all the appropriate attributes, # such as feature names and feature types. - return(xgb.ProxyDMatrix(data = x_batch, label = y_batch)) + return(xgb.DataBatch(data = x_batch, label = y_batch)) } # This moves the iterator back to its beginning @@ -118,5 +118,5 @@ pred_dm <- predict(model, dm) pred_mat <- predict(model, as.matrix(mtcars[, -1])) } \seealso{ -\link{xgb.DataIter}, \link{xgb.ProxyDMatrix}, \link{xgb.QuantileDMatrix.from_iterator} +\link{xgb.DataIter}, \link{xgb.DataBatch}, \link{xgb.QuantileDMatrix.from_iterator} } diff --git a/R-package/man/xgb.QuantileDMatrix.from_iterator.Rd b/R-package/man/xgb.QuantileDMatrix.from_iterator.Rd index 21f24576d..791b5576e 100644 --- a/R-package/man/xgb.QuantileDMatrix.from_iterator.Rd +++ b/R-package/man/xgb.QuantileDMatrix.from_iterator.Rd @@ -60,6 +60,6 @@ For more information, see the guide 'Using XGBoost External Memory Version': \url{https://xgboost.readthedocs.io/en/stable/tutorials/external_memory.html} } \seealso{ -\link{xgb.DataIter}, \link{xgb.ProxyDMatrix}, \link{xgb.ExternalDMatrix}, +\link{xgb.DataIter}, \link{xgb.DataBatch}, \link{xgb.ExternalDMatrix}, \link{xgb.QuantileDMatrix} } diff --git a/R-package/tests/testthat/test_dmatrix.R b/R-package/tests/testthat/test_dmatrix.R index 43b7515bb..50621f241 100644 --- a/R-package/tests/testthat/test_dmatrix.R +++ b/R-package/tests/testthat/test_dmatrix.R @@ -472,7 +472,7 @@ test_that("xgb.DMatrix: ExternalDMatrix produces the same results as regular DMa y = mtcars[, 1] ) ) - iterator_next <- function(iterator_env, proxy_handle) { + iterator_next <- function(iterator_env) { curr_iter <- iterator_env[["iter"]] if (curr_iter >= 2) { return(NULL) @@ -487,7 +487,7 @@ test_that("xgb.DMatrix: ExternalDMatrix produces the same results as regular DMa on.exit({ iterator_env[["iter"]] <- curr_iter + 1 }) - return(xgb.ProxyDMatrix(data = x_batch, label = y_batch)) + return(xgb.DataBatch(data = x_batch, label = y_batch)) } iterator_reset <- function(iterator_env) { iterator_env[["iter"]] <- 0 @@ -546,7 +546,7 @@ test_that("xgb.DMatrix: External QDM produces same results as regular QDM", { y = mtcars[, 1] ) ) - iterator_next <- function(iterator_env, proxy_handle) { + iterator_next <- function(iterator_env) { curr_iter <- iterator_env[["iter"]] if (curr_iter >= 2) { return(NULL) @@ -561,7 +561,7 @@ test_that("xgb.DMatrix: External QDM produces same results as regular QDM", { on.exit({ iterator_env[["iter"]] <- curr_iter + 1 }) - return(xgb.ProxyDMatrix(data = x_batch, label = y_batch)) + return(xgb.DataBatch(data = x_batch, label = y_batch)) } iterator_reset <- function(iterator_env) { iterator_env[["iter"]] <- 0 @@ -604,7 +604,7 @@ test_that("xgb.DMatrix: R errors thrown on DataIterator are thrown back to the u y = mtcars[, 1] ) ) - iterator_next <- function(iterator_env, proxy_handle) { + iterator_next <- function(iterator_env) { curr_iter <- iterator_env[["iter"]] if (curr_iter >= 2) { return(0) @@ -618,7 +618,7 @@ test_that("xgb.DMatrix: R errors thrown on DataIterator are thrown back to the u on.exit({ iterator_env[["iter"]] <- curr_iter + 1 }) - return(xgb.ProxyDMatrix(data = x_batch, label = y_batch)) + return(xgb.DataBatch(data = x_batch, label = y_batch)) } iterator_reset <- function(iterator_env) { iterator_env[["iter"]] <- 0 From 234674a0a62e4d3f4856875297d985e4f73b811d Mon Sep 17 00:00:00 2001 From: Dmitry Razdoburdin Date: Wed, 31 Jan 2024 10:39:48 +0100 Subject: [PATCH 14/16] [sync]. Add partition builder. (#10011) --------- Co-authored-by: Dmitry Razdoburdin <> --- plugin/sycl/common/partition_builder.h | 101 ++++++++++++++++++ tests/cpp/CMakeLists.txt | 34 +++++- tests/cpp/plugin/test_sycl_multiclass_obj.cc | 4 + .../cpp/plugin/test_sycl_partition_builder.cc | 91 ++++++++++++++++ tests/cpp/plugin/test_sycl_predictor.cc | 10 +- tests/cpp/plugin/test_sycl_regression_obj.cc | 4 + 6 files changed, 241 insertions(+), 3 deletions(-) create mode 100644 plugin/sycl/common/partition_builder.h create mode 100644 tests/cpp/plugin/test_sycl_partition_builder.cc diff --git a/plugin/sycl/common/partition_builder.h b/plugin/sycl/common/partition_builder.h new file mode 100644 index 000000000..37d1af241 --- /dev/null +++ b/plugin/sycl/common/partition_builder.h @@ -0,0 +1,101 @@ +/*! + * Copyright 2017-2024 XGBoost contributors + */ +#ifndef PLUGIN_SYCL_COMMON_PARTITION_BUILDER_H_ +#define PLUGIN_SYCL_COMMON_PARTITION_BUILDER_H_ + +#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 + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wtautological-constant-compare" +#include "../../../src/common/column_matrix.h" +#pragma GCC diagnostic pop + +#include "../data.h" + +#include + +namespace xgboost { +namespace sycl { +namespace common { + +// The builder is required for samples partition to left and rights children for set of nodes +class PartitionBuilder { + public: + template + void Init(::sycl::queue* qu, size_t n_nodes, Func funcNTaks) { + qu_ = qu; + nodes_offsets_.resize(n_nodes+1); + result_rows_.resize(2 * n_nodes); + n_nodes_ = n_nodes; + + + nodes_offsets_[0] = 0; + for (size_t i = 1; i < n_nodes+1; ++i) { + nodes_offsets_[i] = nodes_offsets_[i-1] + funcNTaks(i-1); + } + + if (data_.Size() < nodes_offsets_[n_nodes]) { + data_.Resize(qu, nodes_offsets_[n_nodes]); + } + } + + size_t GetNLeftElems(int nid) const { + return result_rows_[2 * nid]; + } + + + size_t GetNRightElems(int nid) const { + return result_rows_[2 * nid + 1]; + } + + // For test purposes only + void SetNLeftElems(int nid, size_t val) { + result_rows_[2 * nid] = val; + } + + // For test purposes only + void SetNRightElems(int nid, size_t val) { + result_rows_[2 * nid + 1] = val; + } + + xgboost::common::Span GetData(int nid) { + return { data_.Data() + nodes_offsets_[nid], nodes_offsets_[nid + 1] - nodes_offsets_[nid] }; + } + + void MergeToArray(size_t nid, + size_t* data_result, + ::sycl::event event) { + size_t n_nodes_total = GetNLeftElems(nid) + GetNRightElems(nid); + if (n_nodes_total > 0) { + const size_t* data = data_.Data() + nodes_offsets_[nid]; + qu_->memcpy(data_result, data, sizeof(size_t) * n_nodes_total, event); + } + } + + protected: + std::vector nodes_offsets_; + std::vector result_rows_; + size_t n_nodes_; + + USMVector parts_size_; + USMVector data_; + + ::sycl::queue* qu_; +}; + +} // namespace common +} // namespace sycl +} // namespace xgboost + + +#endif // PLUGIN_SYCL_COMMON_PARTITION_BUILDER_H_ diff --git a/tests/cpp/CMakeLists.txt b/tests/cpp/CMakeLists.txt index 08862feee..20923519a 100644 --- a/tests/cpp/CMakeLists.txt +++ b/tests/cpp/CMakeLists.txt @@ -14,8 +14,38 @@ if(USE_CUDA) endif() file(GLOB_RECURSE SYCL_TEST_SOURCES "plugin/test_sycl_*.cc") -if(NOT PLUGIN_SYCL) - list(REMOVE_ITEM TEST_SOURCES ${SYCL_TEST_SOURCES}) +list(REMOVE_ITEM TEST_SOURCES ${SYCL_TEST_SOURCES}) + +if(PLUGIN_SYCL) + set(CMAKE_CXX_COMPILER "icpx") + file(GLOB_RECURSE SYCL_TEST_SOURCES "plugin/test_sycl_*.cc") + add_library(plugin_sycl_test OBJECT ${SYCL_TEST_SOURCES}) + + target_include_directories(plugin_sycl_test + PRIVATE + ${gtest_SOURCE_DIR}/include + ${xgboost_SOURCE_DIR}/include + ${xgboost_SOURCE_DIR}/dmlc-core/include + ${xgboost_SOURCE_DIR}/rabit/include) + + target_compile_definitions(plugin_sycl_test PUBLIC -DXGBOOST_USE_SYCL=1) + + target_link_libraries(plugin_sycl_test PUBLIC -fsycl) + + set_target_properties(plugin_sycl_test PROPERTIES + COMPILE_FLAGS -fsycl + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON + POSITION_INDEPENDENT_CODE ON) + if(USE_OPENMP) + find_package(OpenMP REQUIRED) + set_target_properties(plugin_sycl_test PROPERTIES + COMPILE_FLAGS "-fsycl -qopenmp") + endif() + # Get compilation and link flags of plugin_sycl and propagate to testxgboost + target_link_libraries(testxgboost PUBLIC plugin_sycl_test) + # Add all objects of plugin_sycl to testxgboost + target_sources(testxgboost INTERFACE $) endif() if(PLUGIN_FEDERATED) diff --git a/tests/cpp/plugin/test_sycl_multiclass_obj.cc b/tests/cpp/plugin/test_sycl_multiclass_obj.cc index d809ecad3..d306337ac 100644 --- a/tests/cpp/plugin/test_sycl_multiclass_obj.cc +++ b/tests/cpp/plugin/test_sycl_multiclass_obj.cc @@ -2,7 +2,11 @@ * Copyright 2018-2023 XGBoost contributors */ #include +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wtautological-constant-compare" +#pragma GCC diagnostic ignored "-W#pragma-messages" #include +#pragma GCC diagnostic pop #include "../objective/test_multiclass_obj.h" diff --git a/tests/cpp/plugin/test_sycl_partition_builder.cc b/tests/cpp/plugin/test_sycl_partition_builder.cc new file mode 100644 index 000000000..90bc757eb --- /dev/null +++ b/tests/cpp/plugin/test_sycl_partition_builder.cc @@ -0,0 +1,91 @@ +/** + * Copyright 2020-2024 by XGBoost contributors + */ +#include + +#include +#include +#include + +#include "../../../plugin/sycl/common/partition_builder.h" +#include "../../../plugin/sycl/device_manager.h" +#include "../helpers.h" + +namespace xgboost::sycl::common { + +TEST(SyclPartitionBuilder, BasicTest) { + constexpr size_t kNodes = 5; + // Number of rows for each node + std::vector rows = { 5, 5, 10, 1, 2 }; + + DeviceManager device_manager; + auto qu = device_manager.GetQueue(DeviceOrd::SyclDefault()); + PartitionBuilder builder; + builder.Init(&qu, kNodes, [&](size_t i) { + return rows[i]; + }); + + // We test here only the basics, thus syntetic partition builder is adopted + // Number of rows to go left for each node. + std::vector rows_for_left_node = { 2, 0, 7, 1, 2 }; + + size_t first_row_id = 0; + for(size_t nid = 0; nid < kNodes; ++nid) { + size_t n_rows_nodes = rows[nid]; + + auto rid_buff = builder.GetData(nid); + size_t rid_buff_size = rid_buff.size(); + auto* rid_buff_ptr = rid_buff.data(); + + size_t n_left = rows_for_left_node[nid]; + size_t n_right = rows[nid] - n_left; + + qu.submit([&](::sycl::handler& cgh) { + cgh.parallel_for<>(::sycl::range<1>(n_left), [=](::sycl::id<1> pid) { + int row_id = first_row_id + pid[0]; + rid_buff_ptr[pid[0]] = row_id; + }); + }); + qu.wait(); + first_row_id += n_left; + + // We are storing indexes for the right side in the tail of the array to save some memory + qu.submit([&](::sycl::handler& cgh) { + cgh.parallel_for<>(::sycl::range<1>(n_right), [=](::sycl::id<1> pid) { + int row_id = first_row_id + pid[0]; + rid_buff_ptr[rid_buff_size - pid[0] - 1] = row_id; + }); + }); + qu.wait(); + first_row_id += n_right; + + builder.SetNLeftElems(nid, n_left); + builder.SetNRightElems(nid, n_right); + } + + ::sycl::event event; + std::vector v(*std::max_element(rows.begin(), rows.end())); + size_t row_id = 0; + for(size_t nid = 0; nid < kNodes; ++nid) { + builder.MergeToArray(nid, v.data(), event); + qu.wait(); + + // Check that row_id for left side are correct + for(size_t j = 0; j < rows_for_left_node[nid]; ++j) { + ASSERT_EQ(v[j], row_id++); + } + + // Check that row_id for right side are correct + for(size_t j = 0; j < rows[nid] - rows_for_left_node[nid]; ++j) { + ASSERT_EQ(v[rows[nid] - j - 1], row_id++); + } + + // Check that number of left/right rows are correct + size_t n_left = builder.GetNLeftElems(nid); + size_t n_right = builder.GetNRightElems(nid); + ASSERT_EQ(n_left, rows_for_left_node[nid]); + ASSERT_EQ(n_right, (rows[nid] - rows_for_left_node[nid])); + } +} + +} // namespace xgboost::common diff --git a/tests/cpp/plugin/test_sycl_predictor.cc b/tests/cpp/plugin/test_sycl_predictor.cc index f82a9f33d..d5b3a5e5c 100755 --- a/tests/cpp/plugin/test_sycl_predictor.cc +++ b/tests/cpp/plugin/test_sycl_predictor.cc @@ -2,11 +2,19 @@ * Copyright 2017-2023 XGBoost contributors */ #include +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wtautological-constant-compare" +#pragma GCC diagnostic ignored "-W#pragma-messages" #include +#pragma GCC diagnostic pop +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wtautological-constant-compare" #include "../../../src/data/adapter.h" -#include "../../../src/data/proxy_dmatrix.h" #include "../../../src/gbm/gbtree.h" +#pragma GCC diagnostic pop + +#include "../../../src/data/proxy_dmatrix.h" #include "../../../src/gbm/gbtree_model.h" #include "../filesystem.h" // dmlc::TemporaryDirectory #include "../helpers.h" diff --git a/tests/cpp/plugin/test_sycl_regression_obj.cc b/tests/cpp/plugin/test_sycl_regression_obj.cc index 66b4ea508..349415390 100644 --- a/tests/cpp/plugin/test_sycl_regression_obj.cc +++ b/tests/cpp/plugin/test_sycl_regression_obj.cc @@ -2,7 +2,11 @@ * Copyright 2017-2019 XGBoost contributors */ #include +#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 "../helpers.h" From 4dfbe2a8935ca45321e906ab8ee62b5d592d1fe8 Mon Sep 17 00:00:00 2001 From: Philip Hyunsu Cho Date: Wed, 31 Jan 2024 13:20:51 -0800 Subject: [PATCH 15/16] [CI] Test building for 32-bit arch (#10021) * [CI] Test building for 32-bit arch * Update CMakeLists.txt * Fix yaml * Use Debian container * Remove -Werror for 32-bit * Revert "Remove -Werror for 32-bit" This reverts commit c652bc6a037361bcceaf56fb01863210b462793d. * Don't error for overloaded-virtual warning * Ignore some warnings from dmlc-core * Fix compiler warnings * Fix formatting * Apply suggestions from code review Co-authored-by: Jiaming Yuan * Add more cast --------- Co-authored-by: Jiaming Yuan --- .github/workflows/i386.yml | 39 +++++++++++++++++++++++++++++++ CMakeLists.txt | 3 --- src/c_api/c_api.cc | 6 ++--- src/common/column_matrix.h | 10 ++++---- src/common/hist_util.h | 10 ++++---- src/common/ref_resource_view.h | 4 ++-- src/data/gradient_index.cc | 5 ++-- src/data/gradient_index_format.cc | 6 +++-- src/predictor/predictor.cc | 4 ++-- src/tree/hist/hist_cache.h | 8 ++++--- tests/ci_build/Dockerfile.i386 | 8 +++++++ tests/cpp/c_api/test_c_api.cc | 6 ++--- tests/cpp/helpers.cc | 6 ++--- tests/cpp/helpers.h | 4 ++-- 14 files changed, 84 insertions(+), 35 deletions(-) create mode 100644 .github/workflows/i386.yml create mode 100644 tests/ci_build/Dockerfile.i386 diff --git a/.github/workflows/i386.yml b/.github/workflows/i386.yml new file mode 100644 index 000000000..4a4d65b25 --- /dev/null +++ b/.github/workflows/i386.yml @@ -0,0 +1,39 @@ +name: XGBoost-i386-test + +on: [push, pull_request] + +permissions: + contents: read # to fetch code (actions/checkout) + +jobs: + build-32bit: + name: Build 32-bit + runs-on: ubuntu-latest + services: + registry: + image: registry:2 + ports: + - 5000:5000 + steps: + - uses: actions/checkout@v2.5.0 + with: + submodules: 'true' + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver-opts: network=host + - name: Build and push container + uses: docker/build-push-action@v5 + with: + context: . + file: tests/ci_build/Dockerfile.i386 + push: true + tags: localhost:5000/xgboost/build-32bit:latest + cache-from: type=gha + cache-to: type=gha,mode=max + - name: Build XGBoost + run: | + docker run --rm -v $PWD:/workspace -w /workspace \ + -e CXXFLAGS='-Wno-error=overloaded-virtual -Wno-error=maybe-uninitialized -Wno-error=redundant-move' \ + localhost:5000/xgboost/build-32bit:latest \ + tests/ci_build/build_via_cmake.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index 4f240e806..dbfa1cdc2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,9 +39,6 @@ elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") message(FATAL_ERROR "Need Clang 9.0 or newer to build XGBoost") endif() endif() -if(CMAKE_SIZE_OF_VOID_P EQUAL 4) - message(FATAL_ERROR "XGBoost does not support 32-bit archs. Please use 64-bit arch instead.") -endif() include(${xgboost_SOURCE_DIR}/cmake/PrefetchIntrinsics.cmake) find_prefetch_intrinsics() diff --git a/src/c_api/c_api.cc b/src/c_api/c_api.cc index d4cc217d1..0f4748bfe 100644 --- a/src/c_api/c_api.cc +++ b/src/c_api/c_api.cc @@ -1,5 +1,5 @@ /** - * Copyright 2014-2023 by XGBoost Contributors + * Copyright 2014-2024 by XGBoost Contributors */ #include "xgboost/c_api.h" @@ -991,8 +991,8 @@ XGB_DLL int XGBoosterBoostOneIter(BoosterHandle handle, DMatrixHandle dtrain, bs auto *learner = static_cast(handle); auto ctx = learner->Ctx()->MakeCPU(); - auto t_grad = linalg::MakeTensorView(&ctx, common::Span{grad, len}, len); - auto t_hess = linalg::MakeTensorView(&ctx, common::Span{hess, len}, len); + auto t_grad = linalg::MakeTensorView(&ctx, common::Span{grad, static_cast(len)}, len); + auto t_hess = linalg::MakeTensorView(&ctx, common::Span{hess, static_cast(len)}, len); auto s_grad = linalg::ArrayInterfaceStr(t_grad); auto s_hess = linalg::ArrayInterfaceStr(t_hess); diff --git a/src/common/column_matrix.h b/src/common/column_matrix.h index 0862c21ad..440f3c0a8 100644 --- a/src/common/column_matrix.h +++ b/src/common/column_matrix.h @@ -1,5 +1,5 @@ /** - * Copyright 2017-2023, XGBoost Contributors + * Copyright 2017-2024, XGBoost Contributors * \file column_matrix.h * \brief Utility for fast column-wise access * \author Philip Cho @@ -176,7 +176,7 @@ class ColumnMatrix { void SetValid(typename LBitField32::index_type i) { missing.Clear(i); } /** @brief assign the storage to the view. */ void InitView() { - missing = LBitField32{Span{storage.data(), storage.size()}}; + missing = LBitField32{Span{storage.data(), static_cast(storage.size())}}; } void GrowTo(std::size_t n_elements, bool init) { @@ -318,8 +318,8 @@ class ColumnMatrix { common::Span bin_index = { reinterpret_cast(&index_[feature_offset * bins_type_size_]), column_size}; - return std::move(DenseColumnIter{ - bin_index, static_cast(index_base_[fidx]), missing_.missing, feature_offset}); + return DenseColumnIter{ + bin_index, static_cast(index_base_[fidx]), missing_.missing, feature_offset}; } // all columns are dense column and has no missing value @@ -332,7 +332,7 @@ class ColumnMatrix { DispatchBinType(bins_type_size_, [&](auto t) { using ColumnBinT = decltype(t); auto column_index = Span{reinterpret_cast(index_.data()), - index_.size() / sizeof(ColumnBinT)}; + static_cast(index_.size() / sizeof(ColumnBinT))}; ParallelFor(n_samples, n_threads, [&](auto rid) { rid += base_rowid; const size_t ibegin = rid * n_features; diff --git a/src/common/hist_util.h b/src/common/hist_util.h index fbbd15b49..e829752da 100644 --- a/src/common/hist_util.h +++ b/src/common/hist_util.h @@ -1,5 +1,5 @@ /** - * Copyright 2017-2023 by XGBoost Contributors + * Copyright 2017-2024 by XGBoost Contributors * \file hist_util.h * \brief Utility for fast histogram aggregation * \author Philip Cho, Tianqi Chen @@ -113,8 +113,8 @@ class HistogramCuts { auto end = ptrs[column_id + 1]; auto beg = ptrs[column_id]; auto it = std::upper_bound(values.cbegin() + beg, values.cbegin() + end, value); - auto idx = it - values.cbegin(); - idx -= !!(idx == end); + auto idx = static_cast(it - values.cbegin()); + idx -= !!(idx == static_cast(end)); return idx; } @@ -136,8 +136,8 @@ class HistogramCuts { auto beg = ptrs[fidx] + vals.cbegin(); // Truncates the value in case it's not perfectly rounded. auto v = static_cast(common::AsCat(value)); - auto bin_idx = std::lower_bound(beg, end, v) - vals.cbegin(); - if (bin_idx == ptrs.at(fidx + 1)) { + auto bin_idx = static_cast(std::lower_bound(beg, end, v) - vals.cbegin()); + if (bin_idx == static_cast(ptrs.at(fidx + 1))) { bin_idx -= 1; } return bin_idx; diff --git a/src/common/ref_resource_view.h b/src/common/ref_resource_view.h index d4f82e615..61adfdb7b 100644 --- a/src/common/ref_resource_view.h +++ b/src/common/ref_resource_view.h @@ -1,5 +1,5 @@ /** - * Copyright 2023, XGBoost Contributors + * Copyright 2023-2024, XGBoost Contributors */ #ifndef XGBOOST_COMMON_REF_RESOURCE_VIEW_H_ #define XGBOOST_COMMON_REF_RESOURCE_VIEW_H_ @@ -76,7 +76,7 @@ class RefResourceView { [[nodiscard]] size_type size() const { return size_; } // NOLINT [[nodiscard]] size_type size_bytes() const { // NOLINT - return Span{data(), size()}.size_bytes(); + return Span{data(), static_cast(size())}.size_bytes(); } [[nodiscard]] value_type* data() { return ptr_; }; // NOLINT [[nodiscard]] value_type const* data() const { return ptr_; }; // NOLINT diff --git a/src/data/gradient_index.cc b/src/data/gradient_index.cc index 1d3faf945..88a38d5cc 100644 --- a/src/data/gradient_index.cc +++ b/src/data/gradient_index.cc @@ -1,5 +1,5 @@ /** - * Copyright 2017-2023, XGBoost Contributors + * Copyright 2017-2024, XGBoost Contributors * \brief Data type for fast histogram aggregation. */ #include "gradient_index.h" @@ -148,7 +148,8 @@ void GHistIndexMatrix::ResizeIndex(const size_t n_index, const bool isDense) { new_vec = {new_ptr, n_bytes / sizeof(std::uint8_t), malloc_resource}; } this->data = std::move(new_vec); - this->index = common::Index{common::Span{data.data(), data.size()}, t_size}; + this->index = common::Index{common::Span{data.data(), static_cast(data.size())}, + t_size}; }; if ((MaxNumBinPerFeat() - 1 <= static_cast(std::numeric_limits::max())) && diff --git a/src/data/gradient_index_format.cc b/src/data/gradient_index_format.cc index fa8f492ed..542d3aaeb 100644 --- a/src/data/gradient_index_format.cc +++ b/src/data/gradient_index_format.cc @@ -1,5 +1,5 @@ /** - * Copyright 2021-2023 XGBoost contributors + * Copyright 2021-2024 XGBoost contributors */ #include // for size_t #include // for uint8_t @@ -40,7 +40,9 @@ class GHistIndexRawFormat : public SparsePageFormat { return false; } // - index - page->index = common::Index{common::Span{page->data.data(), page->data.size()}, size_type}; + page->index = + common::Index{common::Span{page->data.data(), static_cast(page->data.size())}, + size_type}; // hit count if (!common::ReadVec(fi, &page->hit_count)) { diff --git a/src/predictor/predictor.cc b/src/predictor/predictor.cc index aad33c272..019804eda 100644 --- a/src/predictor/predictor.cc +++ b/src/predictor/predictor.cc @@ -1,5 +1,5 @@ /** - * Copyright 2017-2023 by Contributors + * Copyright 2017-2024 by Contributors */ #include "xgboost/predictor.h" @@ -46,7 +46,7 @@ void ValidateBaseMarginShape(linalg::Tensor const& margin, bst_row_t n void Predictor::InitOutPredictions(const MetaInfo& info, HostDeviceVector* out_preds, const gbm::GBTreeModel& model) const { CHECK_NE(model.learner_model_param->num_output_group, 0); - std::size_t n{model.learner_model_param->OutputLength() * info.num_row_}; + auto n = static_cast(model.learner_model_param->OutputLength() * info.num_row_); const HostDeviceVector* base_margin = info.base_margin_.Data(); if (ctx_->Device().IsCUDA()) { diff --git a/src/tree/hist/hist_cache.h b/src/tree/hist/hist_cache.h index 8a2ba193a..715e1d73e 100644 --- a/src/tree/hist/hist_cache.h +++ b/src/tree/hist/hist_cache.h @@ -1,5 +1,5 @@ /** - * Copyright 2023 by XGBoost Contributors + * Copyright 2023-2024 by XGBoost Contributors */ #ifndef XGBOOST_TREE_HIST_HIST_CACHE_H_ #define XGBOOST_TREE_HIST_HIST_CACHE_H_ @@ -48,11 +48,13 @@ class BoundedHistCollection { BoundedHistCollection() = default; common::GHistRow operator[](std::size_t idx) { auto offset = node_map_.at(idx); - return common::Span{data_->data(), data_->size()}.subspan(offset, n_total_bins_); + return common::Span{data_->data(), static_cast(data_->size())}.subspan( + offset, n_total_bins_); } common::ConstGHistRow operator[](std::size_t idx) const { auto offset = node_map_.at(idx); - return common::Span{data_->data(), data_->size()}.subspan(offset, n_total_bins_); + return common::Span{data_->data(), static_cast(data_->size())}.subspan( + offset, n_total_bins_); } void Reset(bst_bin_t n_total_bins, std::size_t n_cached_nodes) { n_total_bins_ = n_total_bins; diff --git a/tests/ci_build/Dockerfile.i386 b/tests/ci_build/Dockerfile.i386 new file mode 100644 index 000000000..d7c133e2a --- /dev/null +++ b/tests/ci_build/Dockerfile.i386 @@ -0,0 +1,8 @@ +FROM i386/debian:sid + +ENV DEBIAN_FRONTEND noninteractive +SHELL ["/bin/bash", "-c"] # Use Bash as shell + +RUN \ + apt-get update && \ + apt-get install -y tar unzip wget git build-essential ninja-build cmake diff --git a/tests/cpp/c_api/test_c_api.cc b/tests/cpp/c_api/test_c_api.cc index 4491dee92..c4c1f0c45 100644 --- a/tests/cpp/c_api/test_c_api.cc +++ b/tests/cpp/c_api/test_c_api.cc @@ -1,5 +1,5 @@ /** - * Copyright 2019-2023 XGBoost contributors + * Copyright 2019-2024 XGBoost contributors */ #include #include @@ -212,8 +212,8 @@ TEST(CAPI, JsonModelIO) { bst_ulong saved_len{0}; XGBoosterSaveModelToBuffer(handle, R"({"format": "ubj"})", &saved_len, &saved); ASSERT_EQ(len, saved_len); - auto l = StringView{data, len}; - auto r = StringView{saved, saved_len}; + auto l = StringView{data, static_cast(len)}; + auto r = StringView{saved, static_cast(saved_len)}; ASSERT_EQ(l.size(), r.size()); ASSERT_EQ(l, r); diff --git a/tests/cpp/helpers.cc b/tests/cpp/helpers.cc index 97db9dbd8..6ce362f46 100644 --- a/tests/cpp/helpers.cc +++ b/tests/cpp/helpers.cc @@ -1,5 +1,5 @@ /** - * Copyright 2016-2023 by XGBoost contributors + * Copyright 2016-2024 by XGBoost contributors */ #include "helpers.h" @@ -216,7 +216,7 @@ SimpleLCG::StateType SimpleLCG::Max() const { return max(); } static_assert(SimpleLCG::max() - SimpleLCG::min()); void RandomDataGenerator::GenerateLabels(std::shared_ptr p_fmat) const { - RandomDataGenerator{p_fmat->Info().num_row_, this->n_targets_, 0.0f}.GenerateDense( + RandomDataGenerator{static_cast(p_fmat->Info().num_row_), this->n_targets_, 0.0f}.GenerateDense( p_fmat->Info().labels.Data()); CHECK_EQ(p_fmat->Info().labels.Size(), this->rows_ * this->n_targets_); p_fmat->Info().labels.Reshape(this->rows_, this->n_targets_); @@ -458,7 +458,7 @@ void RandomDataGenerator::GenerateCSR( EXPECT_EQ(row_count, dmat->Info().num_row_); if (with_label) { - RandomDataGenerator{dmat->Info().num_row_, this->n_targets_, 0.0f}.GenerateDense( + RandomDataGenerator{static_cast(dmat->Info().num_row_), this->n_targets_, 0.0f}.GenerateDense( dmat->Info().labels.Data()); CHECK_EQ(dmat->Info().labels.Size(), this->rows_ * this->n_targets_); dmat->Info().labels.Reshape(this->rows_, this->n_targets_); diff --git a/tests/cpp/helpers.h b/tests/cpp/helpers.h index 9adda8aed..d603685eb 100644 --- a/tests/cpp/helpers.h +++ b/tests/cpp/helpers.h @@ -1,5 +1,5 @@ /** - * Copyright 2016-2023 by XGBoost contributors + * Copyright 2016-2024 by XGBoost contributors */ #pragma once @@ -238,7 +238,7 @@ class RandomDataGenerator { bst_bin_t bins_{0}; std::vector ft_; - bst_cat_t max_cat_; + bst_cat_t max_cat_{32}; Json ArrayInterfaceImpl(HostDeviceVector* storage, size_t rows, size_t cols) const; From 662854c7d75ef1ec543ee0db73098227de5be59c Mon Sep 17 00:00:00 2001 From: david-cortes Date: Thu, 1 Feb 2024 22:39:09 +0100 Subject: [PATCH 16/16] [R] Document handling of indexes (#10019) --------- Co-authored-by: Jiaming Yuan --- R-package/R/xgb.DMatrix.R | 8 +++++++- R-package/man/xgb.DMatrix.Rd | 10 ++++++++-- R-package/man/xgb.DataBatch.Rd | 10 ++++++++-- doc/R-package/index.rst | 9 +++++++++ doc/R-package/index_base.rst | 29 +++++++++++++++++++++++++++++ 5 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 doc/R-package/index_base.rst diff --git a/R-package/R/xgb.DMatrix.R b/R-package/R/xgb.DMatrix.R index a4c476dbc..ba0686cf9 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -33,7 +33,8 @@ #' \item Binary files generated by \link{xgb.DMatrix.save}, passed as a path to the file. These are #' \bold{not} supported for xgb.QuantileDMatrix'. #' } -#' @param label Label of the training data. +#' @param label Label of the training data. For classification problems, should be passed encoded as +#' integers with numeration starting at zero. #' @param weight Weight for each instance. #' #' Note that, for ranking task, weights are per-group. In ranking task, one weight @@ -69,6 +70,11 @@ #' Note that, while categorical types are treated differently from the rest for model fitting #' purposes, the other types do not influence the generated model, but have effects in other #' functionalities such as feature importances. +#' +#' \bold{Important}: categorical features, if specified manually through `feature_types`, must +#' be encoded as integers with numeration starting at zero, and the same encoding needs to be +#' applied when passing data to `predict`. Even if passing `factor` types, the encoding will +#' not be saved, so make sure that `factor` columns passed to `predict` have the same `levels`. #' @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. diff --git a/R-package/man/xgb.DMatrix.Rd b/R-package/man/xgb.DMatrix.Rd index 8e108a2fa..d18270733 100644 --- a/R-package/man/xgb.DMatrix.Rd +++ b/R-package/man/xgb.DMatrix.Rd @@ -66,7 +66,8 @@ supported for xgb.QuantileDMatrix'. \bold{not} supported for xgb.QuantileDMatrix'. }} -\item{label}{Label of the training data.} +\item{label}{Label of the training data. For classification problems, should be passed encoded as +integers with numeration starting at zero.} \item{weight}{Weight for each instance. @@ -109,7 +110,12 @@ with the following possible values:\itemize{ Note that, while categorical types are treated differently from the rest for model fitting purposes, the other types do not influence the generated model, but have effects in other -functionalities such as feature importances.} +functionalities such as feature importances. + +\bold{Important}: categorical features, if specified manually through \code{feature_types}, must +be encoded as integers with numeration starting at zero, and the same encoding needs to be +applied when passing data to \code{predict}. Even if passing \code{factor} types, the encoding will +not be saved, so make sure that \code{factor} columns passed to \code{predict} have the same \code{levels}.} \item{nthread}{Number of threads used for creating DMatrix.} diff --git a/R-package/man/xgb.DataBatch.Rd b/R-package/man/xgb.DataBatch.Rd index 0eeb117e8..063b82b03 100644 --- a/R-package/man/xgb.DataBatch.Rd +++ b/R-package/man/xgb.DataBatch.Rd @@ -33,7 +33,8 @@ conversions applied to it. See the documentation for parameter \code{data} in \item CSR matrices, as class \code{dgRMatrix} from package \code{Matrix}. }} -\item{label}{Label of the training data.} +\item{label}{Label of the training data. For classification problems, should be passed encoded as +integers with numeration starting at zero.} \item{weight}{Weight for each instance. @@ -69,7 +70,12 @@ with the following possible values:\itemize{ Note that, while categorical types are treated differently from the rest for model fitting purposes, the other types do not influence the generated model, but have effects in other -functionalities such as feature importances.} +functionalities such as feature importances. + +\bold{Important}: categorical features, if specified manually through \code{feature_types}, must +be encoded as integers with numeration starting at zero, and the same encoding needs to be +applied when passing data to \code{predict}. Even if passing \code{factor} types, the encoding will +not be saved, so make sure that \code{factor} columns passed to \code{predict} have the same \code{levels}.} \item{group}{Group size for all ranking group.} diff --git a/doc/R-package/index.rst b/doc/R-package/index.rst index ebd49bb9c..8a27d0174 100644 --- a/doc/R-package/index.rst +++ b/doc/R-package/index.rst @@ -26,3 +26,12 @@ Tutorials Introduction to XGBoost in R Understanding your dataset with XGBoost + +************ +Other topics +************ + +.. toctree:: + :maxdepth: 2 + :titlesonly: + Handling of indexable elements diff --git a/doc/R-package/index_base.rst b/doc/R-package/index_base.rst new file mode 100644 index 000000000..495b2e760 --- /dev/null +++ b/doc/R-package/index_base.rst @@ -0,0 +1,29 @@ +.. _index_base: + +Handling of indexable elements +============================== + +There are many functionalities in XGBoost which refer to indexable elements in a countable set, such as boosting rounds / iterations / trees in a model (which can be referred to by number), classes, categories / levels in categorical features, among others. + +XGBoost, being written in C++, uses base-0 indexing and considers ranges / sequences to be inclusive of the left end but not the right one - for example, a range (0, 3) would include the first three elements, numbered 0, 1, and 2. + +The Python interface uses this same logic, since this is also the way that indexing in Python works, but other languages like R have different logic. In R, indexing is base-1 and ranges / sequences are inclusive of both ends - for example, to refer to the first three elements in a sequence, the interval would be written as (1, 3), and the elements numbered 1, 2, and 3. + +In order to provide a more idiomatic R interface, XGBoost adjusts its user-facing R interface to follow this and similar R conventions, but internally, it needs to convert all these numbers to the format that the C interface uses. This is made more problematic by the fact that models are meant to be serializable and loadable in other interfaces, which will have different indexing logic. + +The following adjustments are made in the R interface: + +- Slicing method for DMatrix, which takes an array of integers, is converted to base-0 indexing by subtracting 1 from each element. Note that this is done in the C-level wrapper function for R, unlike all other conversions which are done in R before being passed to C. +- Slicing method for Booster takes a sequence defined by start, end, and step. The R interface is made to work the same way as R's ``seq`` from the user's POV, so it always adjusts the left end by subtracting one, and depending on whether the step size ends exactly or not at the right end, will also adjust the right end to be non-inclusive in C indexing. +- Parameter ``iterationrange`` in ``predict`` is also made to behave the same way as R's ``seq``. Since it doesn't have a step size, just adjusting the left end by subtracting 1 suffices here. +- ``best_iteration``, depending on the context, might be stored as both a C-level booster attribute, and as an R attribute. Since the C-level attributes are shared across interfaces and used in prediction methods, in order to improve compatibility, it leaves this C-level attribute in base-0 indexing, but the R attribute, if present, will be adjusted to base-1 indexing. Note that the ``predict`` method in R and other interfaces will look at the C-level attribute only. +- Other references to iteration numbers or boosting rounds, such as when printing metrics or saving model snapshots, also follow base-1 indexing. These other references are coded entirely in R, as the C-level functions do not handle such functionalities. +- Terminal leaf / node numbers are returned in base-0 indexing, just like they come from the C interface. +- Tree numbers in plots follow base-1 indexing. Note that these are only displayed when producing these plots through the R interface's own handling of DiagrammeR objects, but not when using the C-level GraphViz 'dot' format generator for plots. +- Feature numbers when producing feature importances, JSONs, trees-to-tables, and SHAP; are all following base-0 indexing. +- Categorical features are defined in R as a ``factor`` type which encodes with base-1 indexing. When categorical features are passed as R ``factor`` types, the conversion is done automatically to base-0 indexing, but if the user whishes to manually supply categorical features as already-encoded integers, then those integers need to already be in base-0 encoding. +- Categorical levels (categories) in outputs such as plots, JSONs, and trees-to-tables; are also referred to using base-0 indexing, regardless of whether they went into the model as integers or as ``factor``-typed columns. +- Categorical labels for DMatrices do not undergo any extra processing - the user must supply base-0 encoded labels. +- A function to retrieve class-specific coefficients when using the linear coefficients history callback takes a class index parameter, which also does not undergo any conversion (i.e. user must pass a base-0 index), in order to match with the label logic - that is, the same class index will refer to the class encoded with that number in the DMatrix ``label`` field. + +New additions to the R interface that take on indexable elements should be mindful of these conventions and try to mimic R's behavior as much as possible.