From 761e8a43d8ba73e8d226833fd028ad4b6d1cdfa1 Mon Sep 17 00:00:00 2001 From: Rong Ou Date: Sat, 25 Feb 2023 13:25:58 -0800 Subject: [PATCH 1/3] Support column split in `approx` tree method --- include/xgboost/data.h | 5 ++ src/tree/hist/evaluate_splits.h | 21 +++++++- src/tree/hist/histogram.h | 2 +- src/tree/updater_approx.cc | 4 +- tests/cpp/tree/test_histmaker.cc | 87 ++++++++++++++++++++++++++------ 5 files changed, 100 insertions(+), 19 deletions(-) diff --git a/include/xgboost/data.h b/include/xgboost/data.h index 00228b145d4e..ec78c588d5d9 100644 --- a/include/xgboost/data.h +++ b/include/xgboost/data.h @@ -529,6 +529,11 @@ class DMatrix { return Info().num_nonzero_ == Info().num_row_ * Info().num_col_; } + /*! \brief Whether the data is split row-wise. */ + bool IsRowSplit() const { + return Info().data_split_mode == DataSplitMode::kRow; + } + /*! \brief Whether the data is split column-wise. */ bool IsColumnSplit() const { return Info().data_split_mode == DataSplitMode::kCol; diff --git a/src/tree/hist/evaluate_splits.h b/src/tree/hist/evaluate_splits.h index 5d7f3b193c71..c7fdb5f7cd26 100644 --- a/src/tree/hist/evaluate_splits.h +++ b/src/tree/hist/evaluate_splits.h @@ -38,6 +38,7 @@ class HistEvaluator { TrainParam param_; std::shared_ptr column_sampler_; TreeEvaluator tree_evaluator_; + bool is_col_split_{false}; FeatureInteractionConstraintHost interaction_constraints_; std::vector snode_; @@ -355,7 +356,24 @@ class HistEvaluator { tloc_candidates[n_threads * nidx_in_set + tidx].split); } } + + if (is_col_split_) { + // With column-wise data split, we gather the best splits from all the workers and update the + // expand entries accordingly. + auto const world = collective::GetWorldSize(); + auto const rank = collective::GetRank(); + auto const num_entries = entries.size(); + std::vector buffer{num_entries * world}; + std::copy_n(entries.cbegin(), num_entries, buffer.begin() + num_entries * rank); + collective::Allgather(buffer.data(), buffer.size() * sizeof(ExpandEntry)); + for (auto nidx_in_set = 0; nidx_in_set < entries.size(); ++nidx_in_set) { + for (auto widx = 0; widx < world; ++widx) { + entries[nidx_in_set].split.Update(buffer[widx * num_entries + nidx_in_set].split); + } + } + } } + // Add splits to tree, handles all statistic void ApplyTreeSplit(ExpandEntry const& candidate, RegTree *p_tree) { auto evaluator = tree_evaluator_.GetEvaluator(); @@ -429,7 +447,8 @@ class HistEvaluator { std::shared_ptr sampler) : ctx_{ctx}, param_{param}, column_sampler_{std::move(sampler)}, - tree_evaluator_{param, static_cast(info.num_col_), Context::kCpuId} { + tree_evaluator_{param, static_cast(info.num_col_), Context::kCpuId}, + is_col_split_{info.data_split_mode == DataSplitMode::kCol} { interaction_constraints_.Configure(param, info.num_col_); column_sampler_->Init(ctx, info.num_col_, info.feature_weights.HostVector(), param_.colsample_bynode, param_.colsample_bylevel, diff --git a/src/tree/hist/histogram.h b/src/tree/hist/histogram.h index df5a85633a99..4e64cbd7533b 100644 --- a/src/tree/hist/histogram.h +++ b/src/tree/hist/histogram.h @@ -98,7 +98,7 @@ class HistogramBuilder { std::vector const &nodes_for_explicit_hist_build, std::vector const &nodes_for_subtraction_trick, RegTree const *p_tree) { - if (is_distributed_) { + if (is_distributed_ && !is_col_split_) { this->AddHistRowsDistributed(starting_index, sync_count, nodes_for_explicit_hist_build, nodes_for_subtraction_trick, p_tree); } else { diff --git a/src/tree/updater_approx.cc b/src/tree/updater_approx.cc index ef5de7b7db28..4a939ff02b15 100644 --- a/src/tree/updater_approx.cc +++ b/src/tree/updater_approx.cc @@ -90,7 +90,9 @@ class GloablApproxBuilder { for (auto const &g : gpair) { root_sum.Add(g); } - collective::Allreduce(reinterpret_cast(&root_sum), 2); + if (p_fmat->IsRowSplit()) { + collective::Allreduce(reinterpret_cast(&root_sum), 2); + } std::vector nodes{best}; size_t i = 0; auto space = ConstructHistSpace(partitioner_, nodes); diff --git a/tests/cpp/tree/test_histmaker.cc b/tests/cpp/tree/test_histmaker.cc index 17dcb4c93261..cd49eeec1b74 100644 --- a/tests/cpp/tree/test_histmaker.cc +++ b/tests/cpp/tree/test_histmaker.cc @@ -1,5 +1,4 @@ #include - #include #include @@ -8,26 +7,34 @@ namespace xgboost { namespace tree { -TEST(GrowHistMaker, InteractionConstraint) { - size_t constexpr kRows = 32; - size_t constexpr kCols = 16; - - Context ctx; - - auto p_dmat = RandomDataGenerator{kRows, kCols, 0.6f}.Seed(3).GenerateDMatrix(); +std::shared_ptr GenerateDMatrix(std::size_t rows, std::size_t cols){ + return RandomDataGenerator{rows, cols, 0.6f}.Seed(3).GenerateDMatrix(); +} - HostDeviceVector gradients (kRows); - std::vector& h_gradients = gradients.HostVector(); +std::unique_ptr> GenerateGradients(std::size_t rows) { + auto p_gradients = std::make_unique>(rows); + auto& h_gradients = p_gradients->HostVector(); xgboost::SimpleLCG gen; xgboost::SimpleRealUniformDistribution dist(0.0f, 1.0f); - for (size_t i = 0; i < kRows; ++i) { - bst_float grad = dist(&gen); - bst_float hess = dist(&gen); - h_gradients[i] = GradientPair(grad, hess); + for (auto i = 0; i < rows; ++i) { + auto grad = dist(&gen); + auto hess = dist(&gen); + h_gradients[i] = GradientPair{grad, hess}; } + return p_gradients; +} + +TEST(GrowHistMaker, InteractionConstraint) +{ + auto constexpr kRows = 32; + auto constexpr kCols = 16; + auto p_dmat = GenerateDMatrix(kRows, kCols); + auto p_gradients = GenerateGradients(kRows); + + Context ctx; { // With constraints RegTree tree; @@ -39,7 +46,7 @@ TEST(GrowHistMaker, InteractionConstraint) { {"interaction_constraints", "[[0, 1]]"}, {"num_feature", std::to_string(kCols)}}); std::vector> position(1); - updater->Update(&gradients, p_dmat.get(), position, {&tree}); + updater->Update(p_gradients.get(), p_dmat.get(), position, {&tree}); ASSERT_EQ(tree.NumExtraNodes(), 4); ASSERT_EQ(tree[0].SplitIndex(), 1); @@ -56,7 +63,7 @@ TEST(GrowHistMaker, InteractionConstraint) { TreeUpdater::Create("grow_histmaker", &ctx, ObjInfo{ObjInfo::kRegression})}; updater->Configure(Args{{"num_feature", std::to_string(kCols)}}); std::vector> position(1); - updater->Update(&gradients, p_dmat.get(), position, {&tree}); + updater->Update(p_gradients.get(), p_dmat.get(), position, {&tree}); ASSERT_EQ(tree.NumExtraNodes(), 10); ASSERT_EQ(tree[0].SplitIndex(), 1); @@ -66,5 +73,53 @@ TEST(GrowHistMaker, InteractionConstraint) { } } +namespace { +void TestColumnSplit(int32_t rows, int32_t cols, RegTree const& expected_tree) { + auto p_dmat = GenerateDMatrix(rows, cols); + auto p_gradients = GenerateGradients(rows); + Context ctx; + std::unique_ptr updater{ + TreeUpdater::Create("grow_histmaker", &ctx, ObjInfo{ObjInfo::kRegression})}; + updater->Configure(Args{{"num_feature", std::to_string(cols)}}); + std::vector> position(1); + + std::unique_ptr sliced{ + p_dmat->SliceCol(collective::GetWorldSize(), collective::GetRank())}; + + RegTree tree; + tree.param.num_feature = cols; + updater->Update(p_gradients.get(), sliced.get(), position, {&tree}); + + EXPECT_EQ(tree.NumExtraNodes(), 10); + EXPECT_EQ(tree[0].SplitIndex(), 1); + + EXPECT_NE(tree[tree[0].LeftChild()].SplitIndex(), 0); + EXPECT_NE(tree[tree[0].RightChild()].SplitIndex(), 0); + + EXPECT_EQ(tree, expected_tree); +} +} // anonymous namespace + +TEST(GrowHistMaker, ColumnSplit) { + auto constexpr kRows = 32; + auto constexpr kCols = 16; + + RegTree expected_tree; + expected_tree.param.num_feature = kCols; + { + auto p_dmat = GenerateDMatrix(kRows, kCols); + auto p_gradients = GenerateGradients(kRows); + Context ctx; + std::unique_ptr updater{ + TreeUpdater::Create("grow_histmaker", &ctx, ObjInfo{ObjInfo::kRegression})}; + updater->Configure(Args{{"num_feature", std::to_string(kCols)}}); + std::vector> position(1); + updater->Update(p_gradients.get(), p_dmat.get(), position, {&expected_tree}); + } + + auto constexpr kWorldSize = 2; + RunWithInMemoryCommunicator(kWorldSize, TestColumnSplit, kRows, kCols, std::cref(expected_tree)); +} + } // namespace tree } // namespace xgboost From 7b340ecbc56e51211dea05b972a752b850f9c08c Mon Sep 17 00:00:00 2001 From: Rong Ou Date: Sat, 25 Feb 2023 13:40:38 -0800 Subject: [PATCH 2/3] revere worker and entry index --- src/tree/hist/evaluate_splits.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tree/hist/evaluate_splits.h b/src/tree/hist/evaluate_splits.h index c7fdb5f7cd26..41f7183f2d37 100644 --- a/src/tree/hist/evaluate_splits.h +++ b/src/tree/hist/evaluate_splits.h @@ -366,9 +366,9 @@ class HistEvaluator { std::vector buffer{num_entries * world}; std::copy_n(entries.cbegin(), num_entries, buffer.begin() + num_entries * rank); collective::Allgather(buffer.data(), buffer.size() * sizeof(ExpandEntry)); - for (auto nidx_in_set = 0; nidx_in_set < entries.size(); ++nidx_in_set) { - for (auto widx = 0; widx < world; ++widx) { - entries[nidx_in_set].split.Update(buffer[widx * num_entries + nidx_in_set].split); + for (auto worker = 0; worker < world; ++worker) { + for (auto nidx_in_set = 0; nidx_in_set < entries.size(); ++nidx_in_set) { + entries[nidx_in_set].split.Update(buffer[worker * num_entries + nidx_in_set].split); } } } From a3b9d66eb820e0b2afc0c7dbe0684a86e0cb7215 Mon Sep 17 00:00:00 2001 From: Rong Ou Date: Mon, 27 Feb 2023 09:21:30 -0800 Subject: [PATCH 3/3] add logging for column split --- src/data/data.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/data/data.cc b/src/data/data.cc index 585b2f25282e..d24048a2ab23 100644 --- a/src/data/data.cc +++ b/src/data/data.cc @@ -912,6 +912,7 @@ DMatrix* DMatrix::Load(const std::string& uri, bool silent, DataSplitMode data_s if (!cache_file.empty()) { LOG(FATAL) << "Column-wise data split is not support for external memory."; } + LOG(CONSOLE) << "Splitting data by column"; auto* sliced = dmat->SliceCol(npart, partid); delete dmat; return sliced;