Unify CPU hist sketching (#5880)
This commit is contained in:
@@ -113,346 +113,12 @@ void GHistIndexMatrix::ResizeIndex(const size_t rbegin, const SparsePage& batch,
|
||||
}
|
||||
|
||||
HistogramCuts::HistogramCuts() {
|
||||
monitor_.Init(__FUNCTION__);
|
||||
cut_ptrs_.HostVector().emplace_back(0);
|
||||
}
|
||||
|
||||
// Dispatch to specific builder.
|
||||
void HistogramCuts::Build(DMatrix* dmat, uint32_t const max_num_bins) {
|
||||
auto const& info = dmat->Info();
|
||||
size_t const total = info.num_row_ * info.num_col_;
|
||||
size_t const nnz = info.num_nonzero_;
|
||||
float const sparsity = static_cast<float>(nnz) / static_cast<float>(total);
|
||||
// Use a small number to avoid calling `dmat->GetColumnBatches'.
|
||||
float constexpr kSparsityThreshold = 0.0005;
|
||||
// FIXME(trivialfis): Distributed environment is not supported.
|
||||
if (sparsity < kSparsityThreshold && (!rabit::IsDistributed())) {
|
||||
LOG(INFO) << "Building quantile cut on a sparse dataset.";
|
||||
SparseCuts cuts(this);
|
||||
cuts.Build(dmat, max_num_bins);
|
||||
} else {
|
||||
LOG(INFO) << "Building quantile cut on a dense dataset or distributed environment.";
|
||||
DenseCuts cuts(this);
|
||||
cuts.Build(dmat, max_num_bins);
|
||||
}
|
||||
LOG(INFO) << "Total number of hist bins: " << cut_ptrs_.HostVector().back();
|
||||
}
|
||||
|
||||
bool CutsBuilder::UseGroup(DMatrix* dmat) {
|
||||
auto& info = dmat->Info();
|
||||
return CutsBuilder::UseGroup(info);
|
||||
}
|
||||
|
||||
bool CutsBuilder::UseGroup(MetaInfo const& info) {
|
||||
size_t const num_groups = info.group_ptr_.size() == 0 ?
|
||||
0 : info.group_ptr_.size() - 1;
|
||||
// Use group index for weights?
|
||||
bool const use_group_ind = num_groups != 0 &&
|
||||
(info.weights_.Size() != info.num_row_);
|
||||
return use_group_ind;
|
||||
}
|
||||
|
||||
void SparseCuts::SingleThreadBuild(SparsePage const& page, MetaInfo const& info,
|
||||
uint32_t max_num_bins,
|
||||
bool const use_group_ind,
|
||||
uint32_t beg_col, uint32_t end_col,
|
||||
uint32_t thread_id) {
|
||||
CHECK_GE(end_col, beg_col);
|
||||
|
||||
// Data groups, used in ranking.
|
||||
std::vector<bst_uint> const& group_ptr = info.group_ptr_;
|
||||
auto &local_min_vals = p_cuts_->min_vals_.HostVector();
|
||||
auto &local_cuts = p_cuts_->cut_values_.HostVector();
|
||||
auto &local_ptrs = p_cuts_->cut_ptrs_.HostVector();
|
||||
local_min_vals.resize(end_col - beg_col, 0);
|
||||
|
||||
for (uint32_t col_id = beg_col; col_id < page.Size() && col_id < end_col; ++col_id) {
|
||||
// Using a local variable makes things easier, but at the cost of memory trashing.
|
||||
WQSketch sketch;
|
||||
common::Span<xgboost::Entry const> const column = page[col_id];
|
||||
uint32_t const n_bins = std::min(static_cast<uint32_t>(column.size()),
|
||||
max_num_bins);
|
||||
if (n_bins == 0) {
|
||||
// cut_ptrs_ is initialized with a zero, so there's always an element at the back
|
||||
CHECK_GE(local_ptrs.size(), 1);
|
||||
local_ptrs.emplace_back(local_ptrs.back());
|
||||
continue;
|
||||
}
|
||||
|
||||
sketch.Init(info.num_row_, 1.0 / (n_bins * WQSketch::kFactor));
|
||||
for (auto const& entry : column) {
|
||||
uint32_t weight_ind = 0;
|
||||
if (use_group_ind) {
|
||||
auto row_idx = entry.index;
|
||||
uint32_t group_ind =
|
||||
this->SearchGroupIndFromRow(group_ptr, page.base_rowid + row_idx);
|
||||
weight_ind = group_ind;
|
||||
} else {
|
||||
weight_ind = entry.index;
|
||||
}
|
||||
sketch.Push(entry.fvalue, info.GetWeight(weight_ind));
|
||||
}
|
||||
|
||||
WQSketch::SummaryContainer out_summary;
|
||||
sketch.GetSummary(&out_summary);
|
||||
WQSketch::SummaryContainer summary;
|
||||
summary.Reserve(n_bins + 1);
|
||||
summary.SetPrune(out_summary, n_bins + 1);
|
||||
|
||||
// Can be use data[1] as the min values so that we don't need to
|
||||
// store another array?
|
||||
float mval = summary.data[0].value;
|
||||
local_min_vals[col_id - beg_col] = mval - (fabs(mval) + 1e-5);
|
||||
|
||||
this->AddCutPoint(summary, max_num_bins);
|
||||
|
||||
bst_float cpt = (summary.size > 0) ?
|
||||
summary.data[summary.size - 1].value :
|
||||
local_min_vals[col_id - beg_col];
|
||||
cpt += fabs(cpt) + 1e-5;
|
||||
local_cuts.emplace_back(cpt);
|
||||
|
||||
local_ptrs.emplace_back(local_cuts.size());
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<size_t> SparseCuts::LoadBalance(SparsePage const& page,
|
||||
size_t const nthreads) {
|
||||
/* Some sparse datasets have their mass concentrating on small
|
||||
* number of features. To avoid wating for a few threads running
|
||||
* forever, we here distirbute different number of columns to
|
||||
* different threads according to number of entries. */
|
||||
size_t const total_entries = page.data.Size();
|
||||
size_t const entries_per_thread = common::DivRoundUp(total_entries, nthreads);
|
||||
|
||||
std::vector<size_t> cols_ptr(nthreads+1, 0);
|
||||
size_t count {0};
|
||||
size_t current_thread {1};
|
||||
|
||||
for (size_t col_id = 0; col_id < page.Size(); ++col_id) {
|
||||
auto const column = page[col_id];
|
||||
cols_ptr[current_thread]++; // add one column to thread
|
||||
count += column.size();
|
||||
if (count > entries_per_thread + 1) {
|
||||
current_thread++;
|
||||
count = 0;
|
||||
cols_ptr[current_thread] = cols_ptr[current_thread-1];
|
||||
}
|
||||
}
|
||||
// Idle threads.
|
||||
for (; current_thread < cols_ptr.size() - 1; ++current_thread) {
|
||||
cols_ptr[current_thread+1] = cols_ptr[current_thread];
|
||||
}
|
||||
|
||||
return cols_ptr;
|
||||
}
|
||||
|
||||
void SparseCuts::Build(DMatrix* dmat, uint32_t const max_num_bins) {
|
||||
monitor_.Start(__FUNCTION__);
|
||||
// Use group index for weights?
|
||||
auto use_group = UseGroup(dmat);
|
||||
uint32_t nthreads = omp_get_max_threads();
|
||||
CHECK_GT(nthreads, 0);
|
||||
std::vector<HistogramCuts> cuts_containers(nthreads);
|
||||
std::vector<std::unique_ptr<SparseCuts>> sparse_cuts(nthreads);
|
||||
for (size_t i = 0; i < nthreads; ++i) {
|
||||
sparse_cuts[i].reset(new SparseCuts(&cuts_containers[i]));
|
||||
}
|
||||
|
||||
for (auto const& page : dmat->GetBatches<CSCPage>()) {
|
||||
CHECK_LE(page.Size(), dmat->Info().num_col_);
|
||||
monitor_.Start("Load balance");
|
||||
std::vector<size_t> col_ptr = LoadBalance(page, nthreads);
|
||||
monitor_.Stop("Load balance");
|
||||
// We here decouples the logic between build and parallelization
|
||||
// to simplify things a bit.
|
||||
#pragma omp parallel for num_threads(nthreads) schedule(static)
|
||||
for (omp_ulong i = 0; i < nthreads; ++i) {
|
||||
common::Monitor t_monitor;
|
||||
t_monitor.Init("SingleThreadBuild: " + std::to_string(i));
|
||||
t_monitor.Start(std::to_string(i));
|
||||
sparse_cuts[i]->SingleThreadBuild(page, dmat->Info(), max_num_bins, use_group,
|
||||
col_ptr[i], col_ptr[i+1], i);
|
||||
t_monitor.Stop(std::to_string(i));
|
||||
}
|
||||
|
||||
this->Concat(sparse_cuts, dmat->Info().num_col_);
|
||||
}
|
||||
|
||||
monitor_.Stop(__FUNCTION__);
|
||||
}
|
||||
|
||||
void SparseCuts::Concat(
|
||||
std::vector<std::unique_ptr<SparseCuts>> const& cuts, uint32_t n_cols) {
|
||||
monitor_.Start(__FUNCTION__);
|
||||
uint32_t nthreads = omp_get_max_threads();
|
||||
auto &local_min_vals = p_cuts_->min_vals_.HostVector();
|
||||
auto &local_cuts = p_cuts_->cut_values_.HostVector();
|
||||
auto &local_ptrs = p_cuts_->cut_ptrs_.HostVector();
|
||||
local_min_vals.resize(n_cols, std::numeric_limits<float>::max());
|
||||
size_t min_vals_tail = 0;
|
||||
|
||||
for (uint32_t t = 0; t < nthreads; ++t) {
|
||||
auto& thread_min_vals = cuts[t]->p_cuts_->min_vals_.HostVector();
|
||||
auto& thread_cuts = cuts[t]->p_cuts_->cut_values_.HostVector();
|
||||
auto& thread_ptrs = cuts[t]->p_cuts_->cut_ptrs_.HostVector();
|
||||
|
||||
// concat csc pointers.
|
||||
size_t const old_ptr_size = local_ptrs.size();
|
||||
local_ptrs.resize(
|
||||
thread_ptrs.size() + local_ptrs.size() - 1);
|
||||
size_t const new_icp_size = local_ptrs.size();
|
||||
auto tail = local_ptrs[old_ptr_size-1];
|
||||
for (size_t j = old_ptr_size; j < new_icp_size; ++j) {
|
||||
local_ptrs[j] = tail + thread_ptrs[j-old_ptr_size+1];
|
||||
}
|
||||
// concat csc values
|
||||
size_t const old_iv_size = local_cuts.size();
|
||||
local_cuts.resize(
|
||||
thread_cuts.size() + local_cuts.size());
|
||||
size_t const new_iv_size = local_cuts.size();
|
||||
for (size_t j = old_iv_size; j < new_iv_size; ++j) {
|
||||
local_cuts[j] = thread_cuts[j-old_iv_size];
|
||||
}
|
||||
// merge min values
|
||||
for (size_t j = 0; j < thread_min_vals.size(); ++j) {
|
||||
local_min_vals.at(min_vals_tail + j) =
|
||||
std::min(local_min_vals.at(min_vals_tail + j), thread_min_vals.at(j));
|
||||
}
|
||||
min_vals_tail += thread_min_vals.size();
|
||||
}
|
||||
monitor_.Stop(__FUNCTION__);
|
||||
}
|
||||
|
||||
void DenseCuts::Build(DMatrix* p_fmat, uint32_t max_num_bins) {
|
||||
monitor_.Start(__FUNCTION__);
|
||||
const MetaInfo& info = p_fmat->Info();
|
||||
|
||||
// safe factor for better accuracy
|
||||
std::vector<WQSketch> sketchs;
|
||||
|
||||
const int nthread = omp_get_max_threads();
|
||||
|
||||
unsigned const nstep =
|
||||
static_cast<unsigned>((info.num_col_ + nthread - 1) / nthread);
|
||||
unsigned const ncol = static_cast<unsigned>(info.num_col_);
|
||||
sketchs.resize(info.num_col_);
|
||||
for (auto& s : sketchs) {
|
||||
s.Init(info.num_row_, 1.0 / (max_num_bins * WQSketch::kFactor));
|
||||
}
|
||||
|
||||
// Data groups, used in ranking.
|
||||
std::vector<bst_uint> const& group_ptr = info.group_ptr_;
|
||||
size_t const num_groups = group_ptr.size() == 0 ? 0 : group_ptr.size() - 1;
|
||||
// Use group index for weights?
|
||||
bool const use_group = UseGroup(p_fmat);
|
||||
const bool isDense = p_fmat->IsDense();
|
||||
for (const auto &batch : p_fmat->GetBatches<SparsePage>()) {
|
||||
size_t group_ind = 0;
|
||||
if (use_group) {
|
||||
group_ind = this->SearchGroupIndFromRow(group_ptr, batch.base_rowid);
|
||||
}
|
||||
#pragma omp parallel num_threads(nthread) firstprivate(group_ind, use_group)
|
||||
{
|
||||
CHECK_EQ(nthread, omp_get_num_threads());
|
||||
auto tid = static_cast<unsigned>(omp_get_thread_num());
|
||||
unsigned begin = std::min(nstep * tid, ncol);
|
||||
unsigned end = std::min(nstep * (tid + 1), ncol);
|
||||
|
||||
// do not iterate if no columns are assigned to the thread
|
||||
if (begin < end && end <= ncol) {
|
||||
for (size_t i = 0; i < batch.Size(); ++i) { // NOLINT(*)
|
||||
size_t const ridx = batch.base_rowid + i;
|
||||
SparsePage::Inst const inst = batch[i];
|
||||
if (use_group &&
|
||||
group_ptr[group_ind] == ridx &&
|
||||
// maximum equals to weights.size() - 1
|
||||
group_ind < num_groups - 1) {
|
||||
// move to next group
|
||||
group_ind++;
|
||||
}
|
||||
size_t w_idx = use_group ? group_ind : ridx;
|
||||
auto w = info.GetWeight(w_idx);
|
||||
if (isDense) {
|
||||
auto data = inst.data();
|
||||
for (size_t ii = begin; ii < end; ii++) {
|
||||
sketchs[ii].Push(data[ii].fvalue, w);
|
||||
}
|
||||
} else {
|
||||
for (auto const& entry : inst) {
|
||||
if (entry.index >= begin && entry.index < end) {
|
||||
sketchs[entry.index].Push(entry.fvalue, w);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Init(&sketchs, max_num_bins, info.num_row_);
|
||||
monitor_.Stop(__FUNCTION__);
|
||||
}
|
||||
|
||||
/**
|
||||
* \param [in,out] in_sketchs
|
||||
* \param max_num_bins The maximum number bins.
|
||||
* \param max_rows Number of rows in this DMatrix.
|
||||
*/
|
||||
void DenseCuts::Init
|
||||
(std::vector<WQSketch>* in_sketchs, uint32_t max_num_bins, size_t max_rows) {
|
||||
monitor_.Start(__func__);
|
||||
std::vector<WQSketch>& sketchs = *in_sketchs;
|
||||
|
||||
// Compute how many cuts samples we need at each node
|
||||
// Do not require more than the number of total rows in training data
|
||||
// This allows efficient training on wide data
|
||||
size_t global_max_rows = max_rows;
|
||||
rabit::Allreduce<rabit::op::Sum>(&global_max_rows, 1);
|
||||
size_t intermediate_num_cuts =
|
||||
std::min(global_max_rows, static_cast<size_t>(max_num_bins * WQSketch::kFactor));
|
||||
// gather the histogram data
|
||||
rabit::SerializeReducer<WQSketch::SummaryContainer> sreducer;
|
||||
std::vector<WQSketch::SummaryContainer> summary_array;
|
||||
summary_array.resize(sketchs.size());
|
||||
for (size_t i = 0; i < sketchs.size(); ++i) {
|
||||
WQSketch::SummaryContainer out;
|
||||
sketchs[i].GetSummary(&out);
|
||||
summary_array[i].Reserve(intermediate_num_cuts);
|
||||
summary_array[i].SetPrune(out, intermediate_num_cuts);
|
||||
}
|
||||
CHECK_EQ(summary_array.size(), in_sketchs->size());
|
||||
size_t nbytes = WQSketch::SummaryContainer::CalcMemCost(intermediate_num_cuts);
|
||||
// TODO(chenqin): rabit failure recovery assumes no boostrap onetime call after loadcheckpoint
|
||||
// we need to move this allreduce before loadcheckpoint call in future
|
||||
sreducer.Allreduce(dmlc::BeginPtr(summary_array), nbytes, summary_array.size());
|
||||
p_cuts_->min_vals_.HostVector().resize(sketchs.size());
|
||||
|
||||
for (size_t fid = 0; fid < summary_array.size(); ++fid) {
|
||||
WQSketch::SummaryContainer a;
|
||||
a.Reserve(max_num_bins + 1);
|
||||
a.SetPrune(summary_array[fid], max_num_bins + 1);
|
||||
const bst_float mval = a.data[0].value;
|
||||
p_cuts_->min_vals_.HostVector()[fid] = mval - (fabs(mval) + 1e-5);
|
||||
AddCutPoint(a, max_num_bins);
|
||||
// push a value that is greater than anything
|
||||
const bst_float cpt
|
||||
= (a.size > 0) ? a.data[a.size - 1].value : p_cuts_->min_vals_.HostVector()[fid];
|
||||
// this must be bigger than last value in a scale
|
||||
const bst_float last = cpt + (fabs(cpt) + 1e-5);
|
||||
p_cuts_->cut_values_.HostVector().push_back(last);
|
||||
|
||||
// Ensure that every feature gets at least one quantile point
|
||||
CHECK_LE(p_cuts_->cut_values_.HostVector().size(), std::numeric_limits<uint32_t>::max());
|
||||
auto cut_size = static_cast<uint32_t>(p_cuts_->cut_values_.HostVector().size());
|
||||
CHECK_GT(cut_size, p_cuts_->cut_ptrs_.HostVector().back());
|
||||
p_cuts_->cut_ptrs_.HostVector().push_back(cut_size);
|
||||
}
|
||||
monitor_.Stop(__func__);
|
||||
}
|
||||
|
||||
void GHistIndexMatrix::Init(DMatrix* p_fmat, int max_bins) {
|
||||
cut.Build(p_fmat, max_bins);
|
||||
cut = SketchOnDMatrix(p_fmat, max_bins);
|
||||
|
||||
max_num_bins = max_bins;
|
||||
const int32_t nthread = omp_get_max_threads();
|
||||
const uint32_t nbins = cut.Ptrs().back();
|
||||
@@ -1048,12 +714,11 @@ void BuildHistKernel(const std::vector<GradientPair>& gpair,
|
||||
}
|
||||
}
|
||||
|
||||
template<typename GradientSumT>
|
||||
void GHistBuilder<GradientSumT>::BuildHist(const std::vector<GradientPair>& gpair,
|
||||
const RowSetCollection::Elem row_indices,
|
||||
const GHistIndexMatrix& gmat,
|
||||
GHistRowT hist,
|
||||
bool isDense) {
|
||||
template <typename GradientSumT>
|
||||
void GHistBuilder<GradientSumT>::BuildHist(
|
||||
const std::vector<GradientPair> &gpair,
|
||||
const RowSetCollection::Elem row_indices, const GHistIndexMatrix &gmat,
|
||||
GHistRowT hist, bool isDense) {
|
||||
const size_t nrows = row_indices.Size();
|
||||
const size_t no_prefetch_size = Prefetch::NoPrefetchSize(nrows);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user