From 448491ea282be669b7c481b0a935d7fd2b0672e4 Mon Sep 17 00:00:00 2001 From: Tom Veasey Date: Fri, 25 Oct 2019 10:34:59 +0100 Subject: [PATCH 1/3] Reduce peak memory usage and improve estimation --- include/maths/CBoostedTreeImpl.h | 63 ++++++--------- .../CDataFrameAnalyzerTrainingTest.cc | 4 +- lib/maths/CBoostedTreeImpl.cc | 81 +++++++++---------- 3 files changed, 65 insertions(+), 83 deletions(-) diff --git a/include/maths/CBoostedTreeImpl.h b/include/maths/CBoostedTreeImpl.h index 473754950a..df34d758f4 100644 --- a/include/maths/CBoostedTreeImpl.h +++ b/include/maths/CBoostedTreeImpl.h @@ -274,8 +274,8 @@ class MATHS_EXPORT CBoostedTreeImpl final { const CDataFrameCategoryEncoder& encoder, const TRegularization& regularization, const TDoubleVecVec& candidateSplits, + const TSizeVec& featureBag, std::size_t depth, - TSizeVec featureBag, const core::CPackedBitVector& rowMask); //! Only called by split but is public so it's accessible to std::make_shared. @@ -285,15 +285,17 @@ class MATHS_EXPORT CBoostedTreeImpl final { const CDataFrameCategoryEncoder& encoder, const TRegularization& regularization, const TDoubleVecVec& candidateSplits, - std::size_t depth, - TSizeVec featureBag, + const TSizeVec& featureBag, bool isLeftChild, + std::size_t depth, const CBoostedTreeNode& split, const core::CPackedBitVector& parentRowMask); //! Only called by split but is public so it's accessible to std::make_shared. CLeafNodeStatistics(std::size_t id, const CLeafNodeStatistics& parent, const CLeafNodeStatistics& sibling, + const TRegularization& regularization, + const TSizeVec& featureBag, core::CPackedBitVector rowMask); CLeafNodeStatistics(const CLeafNodeStatistics&) = delete; @@ -314,37 +316,34 @@ class MATHS_EXPORT CBoostedTreeImpl final { const CDataFrameCategoryEncoder& encoder, const TRegularization& regularization, const TDoubleVecVec& candidateSplits, - TSizeVec featureBag, + const TSizeVec& featureBag, const CBoostedTreeNode& split, bool leftChildHasFewerRows); //! Order two leaves by decreasing gain in splitting them. bool operator<(const CLeafNodeStatistics& rhs) const { - return this->bestSplitStatistics() < rhs.bestSplitStatistics(); + return m_BestSplit < rhs.m_BestSplit; } //! Get the gain in loss of the best split of this leaf. - double gain() const { return this->bestSplitStatistics().s_Gain; } + double gain() const { return m_BestSplit.s_Gain; } - double curvature() const { - return this->bestSplitStatistics().s_Curvature; - } + double curvature() const { return this->m_BestSplit.s_Curvature; } //! Get the best (feature, feature value) split. TSizeDoublePr bestSplit() const { - const auto& split = this->bestSplitStatistics(); - return {split.s_Feature, split.s_SplitAt}; + return {m_BestSplit.s_Feature, m_BestSplit.s_SplitAt}; } //! Check if the left child has fewer rows than the right child. bool leftChildHasFewerRows() const { - return this->bestSplitStatistics().s_LeftChildHasFewerRows; + return m_BestSplit.s_LeftChildHasFewerRows; } //! Check if we should assign the missing feature rows to the left child //! of the split. bool assignMissingToLeft() const { - return this->bestSplitStatistics().s_AssignMissingToLeft; + return m_BestSplit.s_AssignMissingToLeft; } //! Get the node's identifier. @@ -355,8 +354,7 @@ class MATHS_EXPORT CBoostedTreeImpl final { //! Get the memory used by this object. std::size_t memoryUsage() const { - std::size_t mem{core::CMemory::dynamicSize(m_FeatureBag)}; - mem += core::CMemory::dynamicSize(m_RowMask); + std::size_t mem{core::CMemory::dynamicSize(m_RowMask)}; mem += core::CMemory::dynamicSize(m_Derivatives); mem += core::CMemory::dynamicSize(m_MissingDerivatives); return mem; @@ -367,12 +365,7 @@ class MATHS_EXPORT CBoostedTreeImpl final { //! and \p numberSplitsPerFeature. static std::size_t estimateMemoryUsage(std::size_t numberRows, std::size_t numberCols, - double featureBagFraction, std::size_t numberSplitsPerFeature) { - std::size_t featureBagSize{ - static_cast(std::ceil( - featureBagFraction * static_cast(numberCols - 1))) * - sizeof(std::size_t)}; // We will typically get the close to the best compression for most of the // leaves when the set of splits becomes large, corresponding to the worst // case for memory usage. This is because the rows will be spread over many @@ -381,13 +374,13 @@ class MATHS_EXPORT CBoostedTreeImpl final { std::size_t derivativesSize{(numberCols - 1) * numberSplitsPerFeature * sizeof(SAggregateDerivatives)}; std::size_t missingDerivativesSize{(numberCols - 1) * sizeof(SAggregateDerivatives)}; - return sizeof(CLeafNodeStatistics) + featureBagSize + rowMaskSize + - derivativesSize + missingDerivativesSize; + return sizeof(CLeafNodeStatistics) + rowMaskSize + derivativesSize + missingDerivativesSize; } private: //! \brief Statistics relating to a split of the node. struct SSplitStatistics : private boost::less_than_comparable { + SSplitStatistics() = default; SSplitStatistics(double gain, double curvature, std::size_t feature, @@ -410,12 +403,12 @@ class MATHS_EXPORT CBoostedTreeImpl final { return result.str(); } - double s_Gain; - double s_Curvature; - std::size_t s_Feature; - double s_SplitAt; - bool s_LeftChildHasFewerRows; - bool s_AssignMissingToLeft; + double s_Gain = -INF; + double s_Curvature = 0.0; + std::size_t s_Feature = -1; + double s_SplitAt = INF; + bool s_LeftChildHasFewerRows = true; + bool s_AssignMissingToLeft = true; }; //! \brief Aggregate derivatives. @@ -487,25 +480,17 @@ class MATHS_EXPORT CBoostedTreeImpl final { void addRowDerivatives(const CEncodedDataFrameRowRef& row, SSplitAggregateDerivatives& splitAggregateDerivatives) const; - const SSplitStatistics& bestSplitStatistics() const { - if (m_BestSplit == boost::none) { - m_BestSplit = this->computeBestSplitStatistics(); - } - return *m_BestSplit; - } - - SSplitStatistics computeBestSplitStatistics() const; + SSplitStatistics computeBestSplitStatistics(const TRegularization& regularization, + const TSizeVec& featureBag) const; private: std::size_t m_Id; - const TRegularization& m_Regularization; const TDoubleVecVec& m_CandidateSplits; std::size_t m_Depth; - TSizeVec m_FeatureBag; core::CPackedBitVector m_RowMask; TAggregateDerivativesVecVec m_Derivatives; TAggregateDerivativesVec m_MissingDerivatives; - mutable boost::optional m_BestSplit; + SSplitStatistics m_BestSplit; }; private: diff --git a/lib/api/unittest/CDataFrameAnalyzerTrainingTest.cc b/lib/api/unittest/CDataFrameAnalyzerTrainingTest.cc index 85c0bf9f5e..826dae00a5 100644 --- a/lib/api/unittest/CDataFrameAnalyzerTrainingTest.cc +++ b/lib/api/unittest/CDataFrameAnalyzerTrainingTest.cc @@ -398,7 +398,7 @@ void CDataFrameAnalyzerTrainingTest::testRunBoostedTreeRegressionTraining() { << "ms"); CPPUNIT_ASSERT(core::CProgramCounters::counter( - counter_t::E_DFTPMEstimatedPeakMemoryUsage) < 3900000); + counter_t::E_DFTPMEstimatedPeakMemoryUsage) < 2400000); CPPUNIT_ASSERT(core::CProgramCounters::counter(counter_t::E_DFTPMPeakMemoryUsage) < 300000); CPPUNIT_ASSERT(core::CProgramCounters::counter(counter_t::E_DFTPMTimeToTrain) > 0); CPPUNIT_ASSERT(core::CProgramCounters::counter(counter_t::E_DFTPMTimeToTrain) <= duration); @@ -636,7 +636,7 @@ void CDataFrameAnalyzerTrainingTest::testRunBoostedTreeClassifierTraining() { LOG_DEBUG(<< "time to train = " << core::CProgramCounters::counter(counter_t::E_DFTPMTimeToTrain) << "ms"); CPPUNIT_ASSERT(core::CProgramCounters::counter( - counter_t::E_DFTPMEstimatedPeakMemoryUsage) < 3900000); + counter_t::E_DFTPMEstimatedPeakMemoryUsage) < 2400000); CPPUNIT_ASSERT(core::CProgramCounters::counter(counter_t::E_DFTPMPeakMemoryUsage) < 1200000); CPPUNIT_ASSERT(core::CProgramCounters::counter(counter_t::E_DFTPMTimeToTrain) > 0); CPPUNIT_ASSERT(core::CProgramCounters::counter(counter_t::E_DFTPMTimeToTrain) <= duration); diff --git a/lib/maths/CBoostedTreeImpl.cc b/lib/maths/CBoostedTreeImpl.cc index 702226e06c..acd158a4ac 100644 --- a/lib/maths/CBoostedTreeImpl.cc +++ b/lib/maths/CBoostedTreeImpl.cc @@ -96,17 +96,14 @@ CBoostedTreeImpl::CLeafNodeStatistics::CLeafNodeStatistics( const CDataFrameCategoryEncoder& encoder, const TRegularization& regularization, const TDoubleVecVec& candidateSplits, + const TSizeVec& featureBag, std::size_t depth, - TSizeVec featureBag, const core::CPackedBitVector& rowMask) - : m_Id{id}, m_Regularization{regularization}, m_CandidateSplits{candidateSplits}, - m_Depth{depth}, m_FeatureBag{std::move(featureBag)}, m_RowMask{rowMask} { + : m_Id{id}, m_CandidateSplits{candidateSplits}, m_Depth{depth}, m_RowMask{rowMask} { - std::sort(m_FeatureBag.begin(), m_FeatureBag.end()); LOG_TRACE(<< "row mask = " << m_RowMask); - LOG_TRACE(<< "feature bag = " << core::CContainerPrinter::print(m_FeatureBag)); - this->computeAggregateLossDerivatives(numberThreads, frame, encoder); + m_BestSplit = this->computeBestSplitStatistics(regularization, featureBag); } CBoostedTreeImpl::CLeafNodeStatistics::CLeafNodeStatistics( @@ -116,31 +113,28 @@ CBoostedTreeImpl::CLeafNodeStatistics::CLeafNodeStatistics( const CDataFrameCategoryEncoder& encoder, const TRegularization& regularization, const TDoubleVecVec& candidateSplits, - std::size_t depth, - TSizeVec featureBag, + const TSizeVec& featureBag, bool isLeftChild, + std::size_t depth, const CBoostedTreeNode& split, const core::CPackedBitVector& parentRowMask) - : m_Id{id}, m_Regularization{regularization}, - m_CandidateSplits{candidateSplits}, m_Depth{depth}, m_FeatureBag{std::move(featureBag)} { - - std::sort(m_FeatureBag.begin(), m_FeatureBag.end()); - LOG_TRACE(<< "feature bag = " << core::CContainerPrinter::print(m_FeatureBag)); + : m_Id{id}, m_CandidateSplits{candidateSplits}, m_Depth{depth} { this->computeRowMaskAndAggregateLossDerivatives( numberThreads, frame, encoder, isLeftChild, split, parentRowMask); + m_BestSplit = this->computeBestSplitStatistics(regularization, featureBag); } CBoostedTreeImpl::CLeafNodeStatistics::CLeafNodeStatistics(std::size_t id, const CLeafNodeStatistics& parent, const CLeafNodeStatistics& sibling, + const TRegularization& regularization, + const TSizeVec& featureBag, core::CPackedBitVector rowMask) - : m_Id{id}, m_Regularization{sibling.m_Regularization}, - m_CandidateSplits{sibling.m_CandidateSplits}, m_Depth{sibling.m_Depth}, - m_FeatureBag{sibling.m_FeatureBag}, m_RowMask{std::move(rowMask)} { + : m_Id{id}, m_CandidateSplits{sibling.m_CandidateSplits}, + m_Depth{sibling.m_Depth}, m_RowMask{std::move(rowMask)} { LOG_TRACE(<< "row mask = " << m_RowMask); - LOG_TRACE(<< "feature bag = " << core::CContainerPrinter::print(m_FeatureBag)); m_Derivatives.resize(m_CandidateSplits.size()); m_MissingDerivatives.resize(m_CandidateSplits.size()); @@ -181,10 +175,11 @@ CBoostedTreeImpl::CLeafNodeStatistics::CLeafNodeStatistics(std::size_t id, m_MissingDerivatives[i] = SAggregateDerivatives{count, gradient, curvature}; } } - LOG_TRACE(<< "derivatives = " << core::CContainerPrinter::print(m_Derivatives)); LOG_TRACE(<< "missing derivatives = " << core::CContainerPrinter::print(m_MissingDerivatives)); + + m_BestSplit = this->computeBestSplitStatistics(regularization, featureBag); } auto CBoostedTreeImpl::CLeafNodeStatistics::split(std::size_t leftChildId, @@ -194,30 +189,31 @@ auto CBoostedTreeImpl::CLeafNodeStatistics::split(std::size_t leftChildId, const CDataFrameCategoryEncoder& encoder, const TRegularization& regularization, const TDoubleVecVec& candidateSplits, - TSizeVec featureBag, + const TSizeVec& featureBag, const CBoostedTreeNode& split, bool leftChildHasFewerRows) { if (leftChildHasFewerRows) { auto leftChild = std::make_shared( - leftChildId, numberThreads, frame, encoder, regularization, - candidateSplits, m_Depth + 1, std::move(featureBag), - true /*is left child*/, split, m_RowMask); + leftChildId, numberThreads, frame, encoder, regularization, candidateSplits, + featureBag, true /*is left child*/, m_Depth + 1, split, m_RowMask); core::CPackedBitVector rightChildRowMask{m_RowMask}; rightChildRowMask ^= leftChild->rowMask(); auto rightChild = std::make_shared( - rightChildId, *this, *leftChild, std::move(rightChildRowMask)); + rightChildId, *this, *leftChild, regularization, featureBag, + std::move(rightChildRowMask)); return std::make_pair(leftChild, rightChild); } auto rightChild = std::make_shared( rightChildId, numberThreads, frame, encoder, regularization, candidateSplits, - m_Depth + 1, std::move(featureBag), false /*is left child*/, split, m_RowMask); + featureBag, false /*is left child*/, m_Depth + 1, split, m_RowMask); core::CPackedBitVector leftChildRowMask{m_RowMask}; leftChildRowMask ^= rightChild->rowMask(); auto leftChild = std::make_shared( - leftChildId, *this, *rightChild, std::move(leftChildRowMask)); + leftChildId, *this, *rightChild, regularization, featureBag, + std::move(leftChildRowMask)); return std::make_pair(leftChild, rightChild); } @@ -324,16 +320,17 @@ void CBoostedTreeImpl::CLeafNodeStatistics::addRowDerivatives( } CBoostedTreeImpl::CLeafNodeStatistics::SSplitStatistics -CBoostedTreeImpl::CLeafNodeStatistics::computeBestSplitStatistics() const { +CBoostedTreeImpl::CLeafNodeStatistics::computeBestSplitStatistics(const TRegularization& regularization, + const TSizeVec& featureBag) const { // We have three possible regularization terms we'll use: // 1. Tree size: gamma * "node count" // 2. Sum square weights: lambda * sum{"leaf weight" ^ 2)} // 3. Tree depth: alpha * sum{exp(("depth" / "target depth" - 1.0) / "tolerance")} - SSplitStatistics result{-INF, 0.0, m_FeatureBag.size(), INF, true, true}; + SSplitStatistics result; - for (auto i : m_FeatureBag) { + for (auto i : featureBag) { std::size_t c{m_MissingDerivatives[i].s_Count}; double g{m_MissingDerivatives[i].s_Gradient}; double h{m_MissingDerivatives[i].s_Curvature}; @@ -361,16 +358,16 @@ CBoostedTreeImpl::CLeafNodeStatistics::computeBestSplitStatistics() const { double gain[]{CTools::pow2(gl[ASSIGN_MISSING_TO_LEFT]) / (hl[ASSIGN_MISSING_TO_LEFT] + - m_Regularization.leafWeightPenaltyMultiplier()) + + regularization.leafWeightPenaltyMultiplier()) + CTools::pow2(g - gl[ASSIGN_MISSING_TO_LEFT]) / (h - hl[ASSIGN_MISSING_TO_LEFT] + - m_Regularization.leafWeightPenaltyMultiplier()), + regularization.leafWeightPenaltyMultiplier()), CTools::pow2(gl[ASSIGN_MISSING_TO_RIGHT]) / (hl[ASSIGN_MISSING_TO_RIGHT] + - m_Regularization.leafWeightPenaltyMultiplier()) + + regularization.leafWeightPenaltyMultiplier()) + CTools::pow2(g - gl[ASSIGN_MISSING_TO_RIGHT]) / (h - hl[ASSIGN_MISSING_TO_RIGHT] + - m_Regularization.leafWeightPenaltyMultiplier())}; + regularization.leafWeightPenaltyMultiplier())}; if (gain[ASSIGN_MISSING_TO_LEFT] > maximumGain) { maximumGain = gain[ASSIGN_MISSING_TO_LEFT]; @@ -386,11 +383,11 @@ CBoostedTreeImpl::CLeafNodeStatistics::computeBestSplitStatistics() const { } } - double penaltyForDepth{m_Regularization.penaltyForDepth(m_Depth)}; - double penaltyForDepthPlusOne{m_Regularization.penaltyForDepth(m_Depth + 1)}; - double gain{0.5 * (maximumGain - CTools::pow2(g) / (h + m_Regularization.leafWeightPenaltyMultiplier())) - - m_Regularization.treeSizePenaltyMultiplier() - - m_Regularization.depthPenaltyMultiplier() * + double penaltyForDepth{regularization.penaltyForDepth(m_Depth)}; + double penaltyForDepthPlusOne{regularization.penaltyForDepth(m_Depth + 1)}; + double gain{0.5 * (maximumGain - CTools::pow2(g) / (h + regularization.leafWeightPenaltyMultiplier())) - + regularization.treeSizePenaltyMultiplier() - + regularization.depthPenaltyMultiplier() * (2.0 * penaltyForDepthPlusOne - penaltyForDepth)}; SSplitStatistics candidate{ @@ -546,7 +543,8 @@ std::size_t CBoostedTreeImpl::estimateMemoryUsage(std::size_t numberRows, std::size_t numberColumns) const { // The maximum tree size is defined is the maximum number of leaves minus one. // A binary tree with n + 1 leaves has 2n + 1 nodes in total. - std::size_t maximumNumberNodes{2 * this->maximumTreeSize(numberRows) + 1}; + std::size_t maximumNumberLeaves{this->maximumTreeSize(numberRows) + 1}; + std::size_t maximumNumberNodes{2 * maximumNumberLeaves - 1}; std::size_t forestMemoryUsage{ m_MaximumNumberTrees * (sizeof(TNodeVec) + maximumNumberNodes * sizeof(CBoostedTreeNode))}; @@ -554,9 +552,8 @@ std::size_t CBoostedTreeImpl::estimateMemoryUsage(std::size_t numberRows, numberRows * sizeof(CFloatStorage)}; std::size_t hyperparametersMemoryUsage{numberColumns * sizeof(double)}; std::size_t leafNodeStatisticsMemoryUsage{ - maximumNumberNodes * CLeafNodeStatistics::estimateMemoryUsage( - numberRows, numberColumns, m_FeatureBagFraction, - m_NumberSplitsPerFeature)}; + maximumNumberLeaves * CLeafNodeStatistics::estimateMemoryUsage( + numberRows, numberColumns, m_NumberSplitsPerFeature)}; std::size_t dataTypeMemoryUsage{numberColumns * sizeof(CDataFrameUtils::SDataType)}; std::size_t featureSampleProbabilities{numberColumns * sizeof(double)}; std::size_t missingFeatureMaskMemoryUsage{ @@ -810,7 +807,7 @@ CBoostedTreeImpl::trainTree(core::CDataFrame& frame, TLeafNodeStatisticsPtrQueue leaves; leaves.push(std::make_shared( 0 /*root*/, m_NumberThreads, frame, *m_Encoder, m_Regularization, - candidateSplits, 0 /*depth*/, this->featureBag(), trainingRowMask)); + candidateSplits, this->featureBag(), 0 /*depth*/, trainingRowMask)); // We update local variables because the callback can be expensive if it // requires accessing atomics. From 4772a00f370f3de966b1a83128ec4a5f24757688 Mon Sep 17 00:00:00 2001 From: Tom Veasey Date: Fri, 25 Oct 2019 10:43:57 +0100 Subject: [PATCH 2/3] Docs --- docs/CHANGELOG.asciidoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index 61403bfcbc..2393a9d2c0 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -34,6 +34,8 @@ * Improve performance of boosted tree training for both classification and regression. (See {ml-pull}775[#775].) +* Reduce the peak memory used by boosted tree training and fix an overcounting bug +estimating maximum memory usage. (See {ml-pull}781[#781].) == {es} version 7.5.0 From 6f99cee6036b735a5831716b13ca19df8ff77b2e Mon Sep 17 00:00:00 2001 From: Tom Veasey Date: Fri, 25 Oct 2019 10:46:44 +0100 Subject: [PATCH 3/3] No need to move feature bag --- lib/maths/CBoostedTreeImpl.cc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/maths/CBoostedTreeImpl.cc b/lib/maths/CBoostedTreeImpl.cc index acd158a4ac..e91e86f678 100644 --- a/lib/maths/CBoostedTreeImpl.cc +++ b/lib/maths/CBoostedTreeImpl.cc @@ -852,13 +852,11 @@ CBoostedTreeImpl::trainTree(core::CDataFrame& frame, tree[leaf->id()].split(splitFeature, splitValue, assignMissingToLeft, leaf->gain(), leaf->curvature(), tree); - TSizeVec featureBag{this->featureBag()}; - TLeafNodeStatisticsPtr leftChild; TLeafNodeStatisticsPtr rightChild; std::tie(leftChild, rightChild) = leaf->split(leftChildId, rightChildId, m_NumberThreads, frame, *m_Encoder, - m_Regularization, candidateSplits, std::move(featureBag), + m_Regularization, candidateSplits, this->featureBag(), tree[leaf->id()], leftChildHasFewerRows); scopeMemoryUsage.add(leftChild);