Remove feature grouping (#7018)
Co-authored-by: Kirill Shvets <kirill.shvets@intel.com>
This commit is contained in:
parent
05db6a6c29
commit
5cdaac00c1
@ -187,267 +187,6 @@ void GHistIndexMatrix::Init(DMatrix* p_fmat, int max_bins) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename BinIdxType>
|
|
||||||
static size_t GetConflictCount(const std::vector<bool>& mark,
|
|
||||||
const Column<BinIdxType>& column_input,
|
|
||||||
size_t max_cnt) {
|
|
||||||
size_t ret = 0;
|
|
||||||
if (column_input.GetType() == xgboost::common::kDenseColumn) {
|
|
||||||
const DenseColumn<BinIdxType>& column
|
|
||||||
= static_cast<const DenseColumn<BinIdxType>& >(column_input);
|
|
||||||
for (size_t i = 0; i < column.Size(); ++i) {
|
|
||||||
if ((!column.IsMissing(i)) && mark[i]) {
|
|
||||||
++ret;
|
|
||||||
if (ret > max_cnt) {
|
|
||||||
return max_cnt + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const SparseColumn<BinIdxType>& column
|
|
||||||
= static_cast<const SparseColumn<BinIdxType>& >(column_input);
|
|
||||||
for (size_t i = 0; i < column.Size(); ++i) {
|
|
||||||
if (mark[column.GetRowIdx(i)]) {
|
|
||||||
++ret;
|
|
||||||
if (ret > max_cnt) {
|
|
||||||
return max_cnt + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename BinIdxType>
|
|
||||||
inline void
|
|
||||||
MarkUsed(std::vector<bool>* p_mark, const Column<BinIdxType>& column_input) {
|
|
||||||
std::vector<bool>& mark = *p_mark;
|
|
||||||
if (column_input.GetType() == xgboost::common::kDenseColumn) {
|
|
||||||
const DenseColumn<BinIdxType>& column
|
|
||||||
= static_cast<const DenseColumn<BinIdxType>& >(column_input);
|
|
||||||
for (size_t i = 0; i < column.Size(); ++i) {
|
|
||||||
if (!column.IsMissing(i)) {
|
|
||||||
mark[i] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const SparseColumn<BinIdxType>& column
|
|
||||||
= static_cast<const SparseColumn<BinIdxType>& >(column_input);
|
|
||||||
for (size_t i = 0; i < column.Size(); ++i) {
|
|
||||||
mark[column.GetRowIdx(i)] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename BinIdxType>
|
|
||||||
inline void SetGroup(const unsigned fid, const Column<BinIdxType>& column,
|
|
||||||
const size_t max_conflict_cnt, const std::vector<size_t>& search_groups,
|
|
||||||
std::vector<size_t>* p_group_conflict_cnt,
|
|
||||||
std::vector<std::vector<bool>>* p_conflict_marks,
|
|
||||||
std::vector<std::vector<unsigned>>* p_groups,
|
|
||||||
std::vector<size_t>* p_group_nnz, const size_t cur_fid_nnz, const size_t nrow) {
|
|
||||||
bool need_new_group = true;
|
|
||||||
std::vector<size_t>& group_conflict_cnt = *p_group_conflict_cnt;
|
|
||||||
std::vector<std::vector<bool>>& conflict_marks = *p_conflict_marks;
|
|
||||||
std::vector<std::vector<unsigned>>& groups = *p_groups;
|
|
||||||
std::vector<size_t>& group_nnz = *p_group_nnz;
|
|
||||||
|
|
||||||
// examine each candidate group: is it okay to insert fid?
|
|
||||||
for (auto gid : search_groups) {
|
|
||||||
const size_t rest_max_cnt = max_conflict_cnt - group_conflict_cnt[gid];
|
|
||||||
const size_t cnt = GetConflictCount(conflict_marks[gid], column, rest_max_cnt);
|
|
||||||
if (cnt <= rest_max_cnt) {
|
|
||||||
need_new_group = false;
|
|
||||||
groups[gid].push_back(fid);
|
|
||||||
group_conflict_cnt[gid] += cnt;
|
|
||||||
group_nnz[gid] += cur_fid_nnz - cnt;
|
|
||||||
MarkUsed(&conflict_marks[gid], column);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// create new group if necessary
|
|
||||||
if (need_new_group) {
|
|
||||||
groups.emplace_back();
|
|
||||||
groups.back().push_back(fid);
|
|
||||||
group_conflict_cnt.push_back(0);
|
|
||||||
conflict_marks.emplace_back(nrow, false);
|
|
||||||
MarkUsed(&conflict_marks.back(), column);
|
|
||||||
group_nnz.emplace_back(cur_fid_nnz);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::vector<std::vector<unsigned>>
|
|
||||||
FindGroups(const std::vector<unsigned>& feature_list,
|
|
||||||
const std::vector<size_t>& feature_nnz,
|
|
||||||
const ColumnMatrix& colmat,
|
|
||||||
size_t nrow,
|
|
||||||
const tree::TrainParam& param) {
|
|
||||||
/* Goal: Bundle features together that has little or no "overlap", i.e.
|
|
||||||
only a few data points should have nonzero values for
|
|
||||||
member features.
|
|
||||||
Note that one-hot encoded features will be grouped together. */
|
|
||||||
|
|
||||||
std::vector<std::vector<unsigned>> groups;
|
|
||||||
std::vector<std::vector<bool>> conflict_marks;
|
|
||||||
std::vector<size_t> group_nnz;
|
|
||||||
std::vector<size_t> group_conflict_cnt;
|
|
||||||
const auto max_conflict_cnt
|
|
||||||
= static_cast<size_t>(param.max_conflict_rate * nrow);
|
|
||||||
|
|
||||||
for (auto fid : feature_list) {
|
|
||||||
const size_t cur_fid_nnz = feature_nnz[fid];
|
|
||||||
|
|
||||||
// randomly choose some of existing groups as candidates
|
|
||||||
std::vector<size_t> search_groups;
|
|
||||||
for (size_t gid = 0; gid < groups.size(); ++gid) {
|
|
||||||
if (group_nnz[gid] + cur_fid_nnz <= nrow + max_conflict_cnt) {
|
|
||||||
search_groups.push_back(gid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
std::shuffle(search_groups.begin(), search_groups.end(), common::GlobalRandom());
|
|
||||||
if (param.max_search_group > 0 && search_groups.size() > param.max_search_group) {
|
|
||||||
search_groups.resize(param.max_search_group);
|
|
||||||
}
|
|
||||||
|
|
||||||
BinTypeSize bins_type_size = colmat.GetTypeSize();
|
|
||||||
if (bins_type_size == kUint8BinsTypeSize) {
|
|
||||||
const auto column = colmat.GetColumn<uint8_t>(fid);
|
|
||||||
SetGroup(fid, *(column.get()), max_conflict_cnt, search_groups,
|
|
||||||
&group_conflict_cnt, &conflict_marks, &groups, &group_nnz, cur_fid_nnz, nrow);
|
|
||||||
} else if (bins_type_size == kUint16BinsTypeSize) {
|
|
||||||
const auto column = colmat.GetColumn<uint16_t>(fid);
|
|
||||||
SetGroup(fid, *(column.get()), max_conflict_cnt, search_groups,
|
|
||||||
&group_conflict_cnt, &conflict_marks, &groups, &group_nnz, cur_fid_nnz, nrow);
|
|
||||||
} else {
|
|
||||||
CHECK_EQ(bins_type_size, kUint32BinsTypeSize);
|
|
||||||
const auto column = colmat.GetColumn<uint32_t>(fid);
|
|
||||||
SetGroup(fid, *(column.get()), max_conflict_cnt, search_groups,
|
|
||||||
&group_conflict_cnt, &conflict_marks, &groups, &group_nnz, cur_fid_nnz, nrow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return groups;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::vector<std::vector<unsigned>>
|
|
||||||
FastFeatureGrouping(const GHistIndexMatrix& gmat,
|
|
||||||
const ColumnMatrix& colmat,
|
|
||||||
const tree::TrainParam& param) {
|
|
||||||
const size_t nrow = gmat.row_ptr.size() - 1;
|
|
||||||
const size_t nfeature = gmat.cut.Ptrs().size() - 1;
|
|
||||||
|
|
||||||
std::vector<unsigned> feature_list(nfeature);
|
|
||||||
std::iota(feature_list.begin(), feature_list.end(), 0);
|
|
||||||
|
|
||||||
// sort features by nonzero counts, descending order
|
|
||||||
std::vector<size_t> feature_nnz(nfeature);
|
|
||||||
std::vector<unsigned> features_by_nnz(feature_list);
|
|
||||||
gmat.GetFeatureCounts(&feature_nnz[0]);
|
|
||||||
std::sort(features_by_nnz.begin(), features_by_nnz.end(),
|
|
||||||
[&feature_nnz](unsigned a, unsigned b) {
|
|
||||||
return feature_nnz[a] > feature_nnz[b];
|
|
||||||
});
|
|
||||||
|
|
||||||
auto groups_alt1 = FindGroups(feature_list, feature_nnz, colmat, nrow, param);
|
|
||||||
auto groups_alt2 = FindGroups(features_by_nnz, feature_nnz, colmat, nrow, param);
|
|
||||||
auto& groups = (groups_alt1.size() > groups_alt2.size()) ? groups_alt2 : groups_alt1;
|
|
||||||
|
|
||||||
// take apart small, sparse groups, as it won't help speed
|
|
||||||
{
|
|
||||||
std::vector<std::vector<unsigned>> ret;
|
|
||||||
for (const auto& group : groups) {
|
|
||||||
if (group.size() <= 1 || group.size() >= 5) {
|
|
||||||
ret.push_back(group); // keep singleton groups and large (5+) groups
|
|
||||||
} else {
|
|
||||||
size_t nnz = 0;
|
|
||||||
for (auto fid : group) {
|
|
||||||
nnz += feature_nnz[fid];
|
|
||||||
}
|
|
||||||
double nnz_rate = static_cast<double>(nnz) / nrow;
|
|
||||||
// take apart small sparse group, due it will not gain on speed
|
|
||||||
if (nnz_rate <= param.sparse_threshold) {
|
|
||||||
for (auto fid : group) {
|
|
||||||
ret.emplace_back();
|
|
||||||
ret.back().push_back(fid);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ret.push_back(group);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
groups = std::move(ret);
|
|
||||||
}
|
|
||||||
|
|
||||||
// shuffle groups
|
|
||||||
std::shuffle(groups.begin(), groups.end(), common::GlobalRandom());
|
|
||||||
|
|
||||||
return groups;
|
|
||||||
}
|
|
||||||
|
|
||||||
void GHistIndexBlockMatrix::Init(const GHistIndexMatrix& gmat,
|
|
||||||
const ColumnMatrix& colmat,
|
|
||||||
const tree::TrainParam& param) {
|
|
||||||
cut_ = &gmat.cut;
|
|
||||||
|
|
||||||
const size_t nrow = gmat.row_ptr.size() - 1;
|
|
||||||
const uint32_t nbins = gmat.cut.Ptrs().back();
|
|
||||||
|
|
||||||
/* step 1: form feature groups */
|
|
||||||
auto groups = FastFeatureGrouping(gmat, colmat, param);
|
|
||||||
const auto nblock = static_cast<uint32_t>(groups.size());
|
|
||||||
|
|
||||||
/* step 2: build a new CSR matrix for each feature group */
|
|
||||||
std::vector<uint32_t> bin2block(nbins); // lookup table [bin id] => [block id]
|
|
||||||
for (uint32_t group_id = 0; group_id < nblock; ++group_id) {
|
|
||||||
for (auto& fid : groups[group_id]) {
|
|
||||||
const uint32_t bin_begin = gmat.cut.Ptrs()[fid];
|
|
||||||
const uint32_t bin_end = gmat.cut.Ptrs()[fid + 1];
|
|
||||||
for (uint32_t bin_id = bin_begin; bin_id < bin_end; ++bin_id) {
|
|
||||||
bin2block[bin_id] = group_id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::vector<uint32_t>> index_temp(nblock);
|
|
||||||
std::vector<std::vector<size_t>> row_ptr_temp(nblock);
|
|
||||||
for (uint32_t block_id = 0; block_id < nblock; ++block_id) {
|
|
||||||
row_ptr_temp[block_id].push_back(0);
|
|
||||||
}
|
|
||||||
for (size_t rid = 0; rid < nrow; ++rid) {
|
|
||||||
const size_t ibegin = gmat.row_ptr[rid];
|
|
||||||
const size_t iend = gmat.row_ptr[rid + 1];
|
|
||||||
for (size_t j = ibegin; j < iend; ++j) {
|
|
||||||
const uint32_t bin_id = gmat.index[j];
|
|
||||||
const uint32_t block_id = bin2block[bin_id];
|
|
||||||
index_temp[block_id].push_back(bin_id);
|
|
||||||
}
|
|
||||||
for (uint32_t block_id = 0; block_id < nblock; ++block_id) {
|
|
||||||
row_ptr_temp[block_id].push_back(index_temp[block_id].size());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* step 3: concatenate CSR matrices into one (index, row_ptr) pair */
|
|
||||||
std::vector<size_t> index_blk_ptr;
|
|
||||||
std::vector<size_t> row_ptr_blk_ptr;
|
|
||||||
index_blk_ptr.push_back(0);
|
|
||||||
row_ptr_blk_ptr.push_back(0);
|
|
||||||
for (uint32_t block_id = 0; block_id < nblock; ++block_id) {
|
|
||||||
index_.insert(index_.end(), index_temp[block_id].begin(), index_temp[block_id].end());
|
|
||||||
row_ptr_.insert(row_ptr_.end(), row_ptr_temp[block_id].begin(), row_ptr_temp[block_id].end());
|
|
||||||
index_blk_ptr.push_back(index_.size());
|
|
||||||
row_ptr_blk_ptr.push_back(row_ptr_.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
// save shortcut for each block
|
|
||||||
for (uint32_t block_id = 0; block_id < nblock; ++block_id) {
|
|
||||||
Block blk;
|
|
||||||
blk.index_begin = &index_[index_blk_ptr[block_id]];
|
|
||||||
blk.row_ptr_begin = &row_ptr_[row_ptr_blk_ptr[block_id]];
|
|
||||||
blk.index_end = &index_[index_blk_ptr[block_id + 1]];
|
|
||||||
blk.row_ptr_end = &row_ptr_[row_ptr_blk_ptr[block_id + 1]];
|
|
||||||
blocks_.push_back(blk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief fill a histogram by zeros in range [begin, end)
|
* \brief fill a histogram by zeros in range [begin, end)
|
||||||
*/
|
*/
|
||||||
@ -703,71 +442,6 @@ void GHistBuilder<double>::BuildHist(const std::vector<GradientPair>& gpair,
|
|||||||
GHistRow<double> hist,
|
GHistRow<double> hist,
|
||||||
bool isDense);
|
bool isDense);
|
||||||
|
|
||||||
template<typename GradientSumT>
|
|
||||||
void GHistBuilder<GradientSumT>::BuildBlockHist(const std::vector<GradientPair>& gpair,
|
|
||||||
const RowSetCollection::Elem row_indices,
|
|
||||||
const GHistIndexBlockMatrix& gmatb,
|
|
||||||
GHistRowT hist) {
|
|
||||||
static constexpr int kUnroll = 8; // loop unrolling factor
|
|
||||||
const size_t nblock = gmatb.GetNumBlock();
|
|
||||||
const size_t nrows = row_indices.end - row_indices.begin;
|
|
||||||
const size_t rest = nrows % kUnroll;
|
|
||||||
#if defined(_OPENMP)
|
|
||||||
const auto nthread = static_cast<bst_omp_uint>(this->nthread_); // NOLINT
|
|
||||||
#endif // defined(_OPENMP)
|
|
||||||
xgboost::detail::GradientPairInternal<GradientSumT>* p_hist = hist.data();
|
|
||||||
|
|
||||||
dmlc::OMPException exc;
|
|
||||||
#pragma omp parallel for num_threads(nthread) schedule(guided)
|
|
||||||
for (bst_omp_uint bid = 0; bid < nblock; ++bid) {
|
|
||||||
exc.Run([&]() {
|
|
||||||
auto gmat = gmatb[bid];
|
|
||||||
|
|
||||||
for (size_t i = 0; i < nrows - rest; i += kUnroll) {
|
|
||||||
size_t rid[kUnroll];
|
|
||||||
size_t ibegin[kUnroll];
|
|
||||||
size_t iend[kUnroll];
|
|
||||||
GradientPair stat[kUnroll];
|
|
||||||
|
|
||||||
for (int k = 0; k < kUnroll; ++k) {
|
|
||||||
rid[k] = row_indices.begin[i + k];
|
|
||||||
ibegin[k] = gmat.row_ptr[rid[k]];
|
|
||||||
iend[k] = gmat.row_ptr[rid[k] + 1];
|
|
||||||
stat[k] = gpair[rid[k]];
|
|
||||||
}
|
|
||||||
for (int k = 0; k < kUnroll; ++k) {
|
|
||||||
for (size_t j = ibegin[k]; j < iend[k]; ++j) {
|
|
||||||
const uint32_t bin = gmat.index[j];
|
|
||||||
p_hist[bin].Add(stat[k].GetGrad(), stat[k].GetHess());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (size_t i = nrows - rest; i < nrows; ++i) {
|
|
||||||
const size_t rid = row_indices.begin[i];
|
|
||||||
const size_t ibegin = gmat.row_ptr[rid];
|
|
||||||
const size_t iend = gmat.row_ptr[rid + 1];
|
|
||||||
const GradientPair stat = gpair[rid];
|
|
||||||
for (size_t j = ibegin; j < iend; ++j) {
|
|
||||||
const uint32_t bin = gmat.index[j];
|
|
||||||
p_hist[bin].Add(stat.GetGrad(), stat.GetHess());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
exc.Rethrow();
|
|
||||||
}
|
|
||||||
template
|
|
||||||
void GHistBuilder<float>::BuildBlockHist(const std::vector<GradientPair>& gpair,
|
|
||||||
const RowSetCollection::Elem row_indices,
|
|
||||||
const GHistIndexBlockMatrix& gmatb,
|
|
||||||
GHistRow<float> hist);
|
|
||||||
template
|
|
||||||
void GHistBuilder<double>::BuildBlockHist(const std::vector<GradientPair>& gpair,
|
|
||||||
const RowSetCollection::Elem row_indices,
|
|
||||||
const GHistIndexBlockMatrix& gmatb,
|
|
||||||
GHistRow<double> hist);
|
|
||||||
|
|
||||||
|
|
||||||
template<typename GradientSumT>
|
template<typename GradientSumT>
|
||||||
void GHistBuilder<GradientSumT>::SubtractionTrick(GHistRowT self,
|
void GHistBuilder<GradientSumT>::SubtractionTrick(GHistRowT self,
|
||||||
GHistRowT sibling,
|
GHistRowT sibling,
|
||||||
|
|||||||
@ -321,48 +321,8 @@ int32_t XGBOOST_HOST_DEV_INLINE BinarySearchBin(bst_uint begin, bst_uint end,
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct GHistIndexBlock {
|
|
||||||
const size_t* row_ptr;
|
|
||||||
const uint32_t* index;
|
|
||||||
|
|
||||||
inline GHistIndexBlock(const size_t* row_ptr, const uint32_t* index)
|
|
||||||
: row_ptr(row_ptr), index(index) {}
|
|
||||||
|
|
||||||
// get i-th row
|
|
||||||
inline GHistIndexRow operator[](size_t i) const {
|
|
||||||
return {&index[0] + row_ptr[i], row_ptr[i + 1] - row_ptr[i]};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
class ColumnMatrix;
|
class ColumnMatrix;
|
||||||
|
|
||||||
class GHistIndexBlockMatrix {
|
|
||||||
public:
|
|
||||||
void Init(const GHistIndexMatrix& gmat,
|
|
||||||
const ColumnMatrix& colmat,
|
|
||||||
const tree::TrainParam& param);
|
|
||||||
|
|
||||||
inline GHistIndexBlock operator[](size_t i) const {
|
|
||||||
return {blocks_[i].row_ptr_begin, blocks_[i].index_begin};
|
|
||||||
}
|
|
||||||
|
|
||||||
inline size_t GetNumBlock() const {
|
|
||||||
return blocks_.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::vector<size_t> row_ptr_;
|
|
||||||
std::vector<uint32_t> index_;
|
|
||||||
const HistogramCuts* cut_;
|
|
||||||
struct Block {
|
|
||||||
const size_t* row_ptr_begin;
|
|
||||||
const size_t* row_ptr_end;
|
|
||||||
const uint32_t* index_begin;
|
|
||||||
const uint32_t* index_end;
|
|
||||||
};
|
|
||||||
std::vector<Block> blocks_;
|
|
||||||
};
|
|
||||||
|
|
||||||
template<typename GradientSumT>
|
template<typename GradientSumT>
|
||||||
using GHistRow = Span<xgboost::detail::GradientPairInternal<GradientSumT> >;
|
using GHistRow = Span<xgboost::detail::GradientPairInternal<GradientSumT> >;
|
||||||
|
|
||||||
@ -672,11 +632,6 @@ class GHistBuilder {
|
|||||||
const GHistIndexMatrix& gmat,
|
const GHistIndexMatrix& gmat,
|
||||||
GHistRowT hist,
|
GHistRowT hist,
|
||||||
bool isDense);
|
bool isDense);
|
||||||
// same, with feature grouping
|
|
||||||
void BuildBlockHist(const std::vector<GradientPair>& gpair,
|
|
||||||
const RowSetCollection::Elem row_indices,
|
|
||||||
const GHistIndexBlockMatrix& gmatb,
|
|
||||||
GHistRowT hist);
|
|
||||||
// construct a histogram via subtraction trick
|
// construct a histogram via subtraction trick
|
||||||
void SubtractionTrick(GHistRowT self,
|
void SubtractionTrick(GHistRowT self,
|
||||||
GHistRowT sibling,
|
GHistRowT sibling,
|
||||||
|
|||||||
@ -80,8 +80,6 @@ struct TrainParam : public XGBoostParameter<TrainParam> {
|
|||||||
// percentage threshold for treating a feature as sparse
|
// percentage threshold for treating a feature as sparse
|
||||||
// e.g. 0.2 indicates a feature with fewer than 20% nonzeros is considered sparse
|
// e.g. 0.2 indicates a feature with fewer than 20% nonzeros is considered sparse
|
||||||
double sparse_threshold;
|
double sparse_threshold;
|
||||||
// use feature grouping? (default yes)
|
|
||||||
int enable_feature_grouping;
|
|
||||||
// when grouping features, how many "conflicts" to allow.
|
// when grouping features, how many "conflicts" to allow.
|
||||||
// conflict is when an instance has nonzero values for two or more features
|
// conflict is when an instance has nonzero values for two or more features
|
||||||
// default is 0, meaning features should be strictly complementary
|
// default is 0, meaning features should be strictly complementary
|
||||||
@ -199,9 +197,6 @@ struct TrainParam : public XGBoostParameter<TrainParam> {
|
|||||||
// ------ From cpu quantile histogram -------.
|
// ------ From cpu quantile histogram -------.
|
||||||
DMLC_DECLARE_FIELD(sparse_threshold).set_range(0, 1.0).set_default(0.2)
|
DMLC_DECLARE_FIELD(sparse_threshold).set_range(0, 1.0).set_default(0.2)
|
||||||
.describe("percentage threshold for treating a feature as sparse");
|
.describe("percentage threshold for treating a feature as sparse");
|
||||||
DMLC_DECLARE_FIELD(enable_feature_grouping).set_lower_bound(0).set_default(0)
|
|
||||||
.describe("if >0, enable feature grouping to ameliorate work imbalance "
|
|
||||||
"among worker threads");
|
|
||||||
DMLC_DECLARE_FIELD(max_conflict_rate).set_range(0, 1.0).set_default(0)
|
DMLC_DECLARE_FIELD(max_conflict_rate).set_range(0, 1.0).set_default(0)
|
||||||
.describe("when grouping features, how many \"conflicts\" to allow."
|
.describe("when grouping features, how many \"conflicts\" to allow."
|
||||||
"conflict is when an instance has nonzero values for two or more features."
|
"conflict is when an instance has nonzero values for two or more features."
|
||||||
|
|||||||
@ -71,7 +71,7 @@ void QuantileHistMaker::CallBuilderUpdate(const std::unique_ptr<Builder<Gradient
|
|||||||
DMatrix *dmat,
|
DMatrix *dmat,
|
||||||
const std::vector<RegTree *> &trees) {
|
const std::vector<RegTree *> &trees) {
|
||||||
for (auto tree : trees) {
|
for (auto tree : trees) {
|
||||||
builder->Update(gmat_, gmatb_, column_matrix_, gpair, dmat, tree);
|
builder->Update(gmat_, column_matrix_, gpair, dmat, tree);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void QuantileHistMaker::Update(HostDeviceVector<GradientPair> *gpair,
|
void QuantileHistMaker::Update(HostDeviceVector<GradientPair> *gpair,
|
||||||
@ -81,9 +81,6 @@ void QuantileHistMaker::Update(HostDeviceVector<GradientPair> *gpair,
|
|||||||
updater_monitor_.Start("GmatInitialization");
|
updater_monitor_.Start("GmatInitialization");
|
||||||
gmat_.Init(dmat, static_cast<uint32_t>(param_.max_bin));
|
gmat_.Init(dmat, static_cast<uint32_t>(param_.max_bin));
|
||||||
column_matrix_.Init(gmat_, param_.sparse_threshold);
|
column_matrix_.Init(gmat_, param_.sparse_threshold);
|
||||||
if (param_.enable_feature_grouping > 0) {
|
|
||||||
gmatb_.Init(gmat_, column_matrix_, param_);
|
|
||||||
}
|
|
||||||
updater_monitor_.Stop("GmatInitialization");
|
updater_monitor_.Stop("GmatInitialization");
|
||||||
// A proper solution is puting cut matrix in DMatrix, see:
|
// A proper solution is puting cut matrix in DMatrix, see:
|
||||||
// https://github.com/dmlc/xgboost/issues/5143
|
// https://github.com/dmlc/xgboost/issues/5143
|
||||||
@ -295,7 +292,6 @@ void QuantileHistMaker::Builder<GradientSumT>::SetHistRowsAdder(
|
|||||||
template <typename GradientSumT>
|
template <typename GradientSumT>
|
||||||
void QuantileHistMaker::Builder<GradientSumT>::InitRoot(
|
void QuantileHistMaker::Builder<GradientSumT>::InitRoot(
|
||||||
const GHistIndexMatrix &gmat,
|
const GHistIndexMatrix &gmat,
|
||||||
const GHistIndexBlockMatrix &gmatb,
|
|
||||||
const DMatrix& fmat,
|
const DMatrix& fmat,
|
||||||
RegTree *p_tree,
|
RegTree *p_tree,
|
||||||
const std::vector<GradientPair> &gpair_h,
|
const std::vector<GradientPair> &gpair_h,
|
||||||
@ -311,7 +307,7 @@ void QuantileHistMaker::Builder<GradientSumT>::InitRoot(
|
|||||||
int sync_count = 0;
|
int sync_count = 0;
|
||||||
|
|
||||||
hist_rows_adder_->AddHistRows(this, &starting_index, &sync_count, p_tree);
|
hist_rows_adder_->AddHistRows(this, &starting_index, &sync_count, p_tree);
|
||||||
BuildLocalHistograms(gmat, gmatb, p_tree, gpair_h);
|
BuildLocalHistograms(gmat, p_tree, gpair_h);
|
||||||
hist_synchronizer_->SyncHistograms(this, starting_index, sync_count, p_tree);
|
hist_synchronizer_->SyncHistograms(this, starting_index, sync_count, p_tree);
|
||||||
|
|
||||||
this->InitNewNode(CPUExpandEntry::kRootNid, gmat, gpair_h, fmat, *p_tree);
|
this->InitNewNode(CPUExpandEntry::kRootNid, gmat, gpair_h, fmat, *p_tree);
|
||||||
@ -325,7 +321,6 @@ void QuantileHistMaker::Builder<GradientSumT>::InitRoot(
|
|||||||
template<typename GradientSumT>
|
template<typename GradientSumT>
|
||||||
void QuantileHistMaker::Builder<GradientSumT>::BuildLocalHistograms(
|
void QuantileHistMaker::Builder<GradientSumT>::BuildLocalHistograms(
|
||||||
const GHistIndexMatrix &gmat,
|
const GHistIndexMatrix &gmat,
|
||||||
const GHistIndexBlockMatrix &gmatb,
|
|
||||||
RegTree *p_tree,
|
RegTree *p_tree,
|
||||||
const std::vector<GradientPair> &gpair_h) {
|
const std::vector<GradientPair> &gpair_h) {
|
||||||
builder_monitor_.Start("BuildLocalHistograms");
|
builder_monitor_.Start("BuildLocalHistograms");
|
||||||
@ -355,7 +350,7 @@ void QuantileHistMaker::Builder<GradientSumT>::BuildLocalHistograms(
|
|||||||
auto rid_set = RowSetCollection::Elem(start_of_row_set + r.begin(),
|
auto rid_set = RowSetCollection::Elem(start_of_row_set + r.begin(),
|
||||||
start_of_row_set + r.end(),
|
start_of_row_set + r.end(),
|
||||||
nid);
|
nid);
|
||||||
BuildHist(gpair_h, rid_set, gmat, gmatb, hist_buffer_.GetInitializedHist(tid, nid_in_set));
|
BuildHist(gpair_h, rid_set, gmat, hist_buffer_.GetInitializedHist(tid, nid_in_set));
|
||||||
});
|
});
|
||||||
|
|
||||||
builder_monitor_.Stop("BuildLocalHistograms");
|
builder_monitor_.Stop("BuildLocalHistograms");
|
||||||
@ -446,7 +441,6 @@ void QuantileHistMaker::Builder<GradientSumT>::BuildNodeStats(
|
|||||||
template<typename GradientSumT>
|
template<typename GradientSumT>
|
||||||
void QuantileHistMaker::Builder<GradientSumT>::ExpandTree(
|
void QuantileHistMaker::Builder<GradientSumT>::ExpandTree(
|
||||||
const GHistIndexMatrix& gmat,
|
const GHistIndexMatrix& gmat,
|
||||||
const GHistIndexBlockMatrix& gmatb,
|
|
||||||
const ColumnMatrix& column_matrix,
|
const ColumnMatrix& column_matrix,
|
||||||
DMatrix* p_fmat,
|
DMatrix* p_fmat,
|
||||||
RegTree* p_tree,
|
RegTree* p_tree,
|
||||||
@ -456,7 +450,7 @@ void QuantileHistMaker::Builder<GradientSumT>::ExpandTree(
|
|||||||
|
|
||||||
Driver<CPUExpandEntry> driver(static_cast<TrainParam::TreeGrowPolicy>(param_.grow_policy));
|
Driver<CPUExpandEntry> driver(static_cast<TrainParam::TreeGrowPolicy>(param_.grow_policy));
|
||||||
std::vector<CPUExpandEntry> expand;
|
std::vector<CPUExpandEntry> expand;
|
||||||
InitRoot(gmat, gmatb, *p_fmat, p_tree, gpair_h, &num_leaves, &expand);
|
InitRoot(gmat, *p_fmat, p_tree, gpair_h, &num_leaves, &expand);
|
||||||
driver.Push(expand[0]);
|
driver.Push(expand[0]);
|
||||||
|
|
||||||
int depth = 0;
|
int depth = 0;
|
||||||
@ -478,7 +472,7 @@ void QuantileHistMaker::Builder<GradientSumT>::ExpandTree(
|
|||||||
int sync_count = 0;
|
int sync_count = 0;
|
||||||
hist_rows_adder_->AddHistRows(this, &starting_index, &sync_count, p_tree);
|
hist_rows_adder_->AddHistRows(this, &starting_index, &sync_count, p_tree);
|
||||||
if (depth < param_.max_depth) {
|
if (depth < param_.max_depth) {
|
||||||
BuildLocalHistograms(gmat, gmatb, p_tree, gpair_h);
|
BuildLocalHistograms(gmat, p_tree, gpair_h);
|
||||||
hist_synchronizer_->SyncHistograms(this, starting_index, sync_count, p_tree);
|
hist_synchronizer_->SyncHistograms(this, starting_index, sync_count, p_tree);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -506,8 +500,9 @@ void QuantileHistMaker::Builder<GradientSumT>::ExpandTree(
|
|||||||
|
|
||||||
template <typename GradientSumT>
|
template <typename GradientSumT>
|
||||||
void QuantileHistMaker::Builder<GradientSumT>::Update(
|
void QuantileHistMaker::Builder<GradientSumT>::Update(
|
||||||
const GHistIndexMatrix &gmat, const GHistIndexBlockMatrix &gmatb,
|
const GHistIndexMatrix &gmat,
|
||||||
const ColumnMatrix &column_matrix, HostDeviceVector<GradientPair> *gpair,
|
const ColumnMatrix &column_matrix,
|
||||||
|
HostDeviceVector<GradientPair> *gpair,
|
||||||
DMatrix *p_fmat, RegTree *p_tree) {
|
DMatrix *p_fmat, RegTree *p_tree) {
|
||||||
builder_monitor_.Start("Update");
|
builder_monitor_.Start("Update");
|
||||||
|
|
||||||
@ -525,7 +520,7 @@ void QuantileHistMaker::Builder<GradientSumT>::Update(
|
|||||||
|
|
||||||
this->InitData(gmat, *p_fmat, *p_tree, gpair_ptr);
|
this->InitData(gmat, *p_fmat, *p_tree, gpair_ptr);
|
||||||
|
|
||||||
ExpandTree(gmat, gmatb, column_matrix, p_fmat, p_tree, *gpair_ptr);
|
ExpandTree(gmat, column_matrix, p_fmat, p_tree, *gpair_ptr);
|
||||||
|
|
||||||
for (int nid = 0; nid < p_tree->param.num_nodes; ++nid) {
|
for (int nid = 0; nid < p_tree->param.num_nodes; ++nid) {
|
||||||
p_tree->Stat(nid).loss_chg = snode_[nid].best.loss_chg;
|
p_tree->Stat(nid).loss_chg = snode_[nid].best.loss_chg;
|
||||||
|
|||||||
@ -111,7 +111,6 @@ class MemStackAllocator {
|
|||||||
namespace tree {
|
namespace tree {
|
||||||
|
|
||||||
using xgboost::common::GHistIndexMatrix;
|
using xgboost::common::GHistIndexMatrix;
|
||||||
using xgboost::common::GHistIndexBlockMatrix;
|
|
||||||
using xgboost::common::GHistIndexRow;
|
using xgboost::common::GHistIndexRow;
|
||||||
using xgboost::common::HistCollection;
|
using xgboost::common::HistCollection;
|
||||||
using xgboost::common::RowSetCollection;
|
using xgboost::common::RowSetCollection;
|
||||||
@ -245,8 +244,6 @@ class QuantileHistMaker: public TreeUpdater {
|
|||||||
TrainParam param_;
|
TrainParam param_;
|
||||||
// quantized data matrix
|
// quantized data matrix
|
||||||
GHistIndexMatrix gmat_;
|
GHistIndexMatrix gmat_;
|
||||||
// (optional) data matrix with feature grouping
|
|
||||||
GHistIndexBlockMatrix gmatb_;
|
|
||||||
// column accessor
|
// column accessor
|
||||||
ColumnMatrix column_matrix_;
|
ColumnMatrix column_matrix_;
|
||||||
DMatrix const* p_last_dmat_ {nullptr};
|
DMatrix const* p_last_dmat_ {nullptr};
|
||||||
@ -289,7 +286,6 @@ class QuantileHistMaker: public TreeUpdater {
|
|||||||
}
|
}
|
||||||
// update one tree, growing
|
// update one tree, growing
|
||||||
virtual void Update(const GHistIndexMatrix& gmat,
|
virtual void Update(const GHistIndexMatrix& gmat,
|
||||||
const GHistIndexBlockMatrix& gmatb,
|
|
||||||
const ColumnMatrix& column_matrix,
|
const ColumnMatrix& column_matrix,
|
||||||
HostDeviceVector<GradientPair>* gpair,
|
HostDeviceVector<GradientPair>* gpair,
|
||||||
DMatrix* p_fmat,
|
DMatrix* p_fmat,
|
||||||
@ -298,14 +294,9 @@ class QuantileHistMaker: public TreeUpdater {
|
|||||||
inline void BuildHist(const std::vector<GradientPair>& gpair,
|
inline void BuildHist(const std::vector<GradientPair>& gpair,
|
||||||
const RowSetCollection::Elem row_indices,
|
const RowSetCollection::Elem row_indices,
|
||||||
const GHistIndexMatrix& gmat,
|
const GHistIndexMatrix& gmat,
|
||||||
const GHistIndexBlockMatrix& gmatb,
|
|
||||||
GHistRowT hist) {
|
GHistRowT hist) {
|
||||||
if (param_.enable_feature_grouping > 0) {
|
hist_builder_.BuildHist(gpair, row_indices, gmat, hist,
|
||||||
hist_builder_.BuildBlockHist(gpair, row_indices, gmatb, hist);
|
data_layout_ != DataLayout::kSparseData);
|
||||||
} else {
|
|
||||||
hist_builder_.BuildHist(gpair, row_indices, gmat, hist,
|
|
||||||
data_layout_ != DataLayout::kSparseData);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void SubtractionTrick(GHistRowT self,
|
inline void SubtractionTrick(GHistRowT self,
|
||||||
@ -386,12 +377,10 @@ class QuantileHistMaker: public TreeUpdater {
|
|||||||
bool SplitContainsMissingValues(const GradStats e, const NodeEntry& snode);
|
bool SplitContainsMissingValues(const GradStats e, const NodeEntry& snode);
|
||||||
|
|
||||||
void BuildLocalHistograms(const GHistIndexMatrix &gmat,
|
void BuildLocalHistograms(const GHistIndexMatrix &gmat,
|
||||||
const GHistIndexBlockMatrix &gmatb,
|
|
||||||
RegTree *p_tree,
|
RegTree *p_tree,
|
||||||
const std::vector<GradientPair> &gpair_h);
|
const std::vector<GradientPair> &gpair_h);
|
||||||
|
|
||||||
void InitRoot(const GHistIndexMatrix &gmat,
|
void InitRoot(const GHistIndexMatrix &gmat,
|
||||||
const GHistIndexBlockMatrix &gmatb,
|
|
||||||
const DMatrix& fmat,
|
const DMatrix& fmat,
|
||||||
RegTree *p_tree,
|
RegTree *p_tree,
|
||||||
const std::vector<GradientPair> &gpair_h,
|
const std::vector<GradientPair> &gpair_h,
|
||||||
@ -415,7 +404,6 @@ class QuantileHistMaker: public TreeUpdater {
|
|||||||
const std::vector<CPUExpandEntry>& nodes_for_apply_split, RegTree *p_tree);
|
const std::vector<CPUExpandEntry>& nodes_for_apply_split, RegTree *p_tree);
|
||||||
|
|
||||||
void ExpandTree(const GHistIndexMatrix& gmat,
|
void ExpandTree(const GHistIndexMatrix& gmat,
|
||||||
const GHistIndexBlockMatrix& gmatb,
|
|
||||||
const ColumnMatrix& column_matrix,
|
const ColumnMatrix& column_matrix,
|
||||||
DMatrix* p_fmat,
|
DMatrix* p_fmat,
|
||||||
RegTree* p_tree,
|
RegTree* p_tree,
|
||||||
|
|||||||
@ -307,11 +307,10 @@ class QuantileHistMock : public QuantileHistMaker {
|
|||||||
{ {0.23f, 0.24f}, {0.24f, 0.25f}, {0.26f, 0.27f}, {0.27f, 0.28f},
|
{ {0.23f, 0.24f}, {0.24f, 0.25f}, {0.26f, 0.27f}, {0.27f, 0.28f},
|
||||||
{0.27f, 0.29f}, {0.37f, 0.39f}, {0.47f, 0.49f}, {0.57f, 0.59f} };
|
{0.27f, 0.29f}, {0.37f, 0.39f}, {0.47f, 0.49f}, {0.57f, 0.59f} };
|
||||||
RealImpl::InitData(gmat, fmat, tree, &gpair);
|
RealImpl::InitData(gmat, fmat, tree, &gpair);
|
||||||
GHistIndexBlockMatrix dummy;
|
|
||||||
this->hist_.AddHistRow(nid);
|
this->hist_.AddHistRow(nid);
|
||||||
this->hist_.AllocateAllData();
|
this->hist_.AllocateAllData();
|
||||||
this->BuildHist(gpair, this->row_set_collection_[nid],
|
this->BuildHist(gpair, this->row_set_collection_[nid],
|
||||||
gmat, dummy, this->hist_[nid]);
|
gmat, this->hist_[nid]);
|
||||||
|
|
||||||
// Check if number of histogram bins is correct
|
// Check if number of histogram bins is correct
|
||||||
ASSERT_EQ(this->hist_[nid].size(), gmat.cut.Ptrs().back());
|
ASSERT_EQ(this->hist_[nid].size(), gmat.cut.Ptrs().back());
|
||||||
@ -337,8 +336,7 @@ class QuantileHistMock : public QuantileHistMaker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestEvaluateSplit(const GHistIndexBlockMatrix& quantile_index_block,
|
void TestEvaluateSplit(const RegTree& tree) {
|
||||||
const RegTree& tree) {
|
|
||||||
std::vector<GradientPair> row_gpairs =
|
std::vector<GradientPair> row_gpairs =
|
||||||
{ {1.23f, 0.24f}, {0.24f, 0.25f}, {0.26f, 0.27f}, {2.27f, 0.28f},
|
{ {1.23f, 0.24f}, {0.24f, 0.25f}, {0.26f, 0.27f}, {2.27f, 0.28f},
|
||||||
{0.27f, 0.29f}, {0.37f, 0.39f}, {-0.47f, 0.49f}, {0.57f, 0.59f} };
|
{0.27f, 0.29f}, {0.37f, 0.39f}, {-0.47f, 0.49f}, {0.57f, 0.59f} };
|
||||||
@ -353,7 +351,7 @@ class QuantileHistMock : public QuantileHistMaker {
|
|||||||
this->hist_.AddHistRow(0);
|
this->hist_.AddHistRow(0);
|
||||||
this->hist_.AllocateAllData();
|
this->hist_.AllocateAllData();
|
||||||
this->BuildHist(row_gpairs, this->row_set_collection_[0],
|
this->BuildHist(row_gpairs, this->row_set_collection_[0],
|
||||||
gmat, quantile_index_block, this->hist_[0]);
|
gmat, this->hist_[0]);
|
||||||
|
|
||||||
RealImpl::InitNewNode(0, gmat, row_gpairs, *dmat, tree);
|
RealImpl::InitNewNode(0, gmat, row_gpairs, *dmat, tree);
|
||||||
|
|
||||||
@ -419,15 +417,13 @@ class QuantileHistMock : public QuantileHistMaker {
|
|||||||
ASSERT_EQ(this->snode_[0].best.split_value, gmat.cut.Values()[best_split_threshold]);
|
ASSERT_EQ(this->snode_[0].best.split_value, gmat.cut.Values()[best_split_threshold]);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestEvaluateSplitParallel(const GHistIndexBlockMatrix &quantile_index_block,
|
void TestEvaluateSplitParallel(const RegTree &tree) {
|
||||||
const RegTree &tree) {
|
|
||||||
omp_set_num_threads(2);
|
omp_set_num_threads(2);
|
||||||
TestEvaluateSplit(quantile_index_block, tree);
|
TestEvaluateSplit(tree);
|
||||||
omp_set_num_threads(1);
|
omp_set_num_threads(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestApplySplit(const GHistIndexBlockMatrix& quantile_index_block,
|
void TestApplySplit(const RegTree& tree) {
|
||||||
const RegTree& tree) {
|
|
||||||
std::vector<GradientPair> row_gpairs =
|
std::vector<GradientPair> row_gpairs =
|
||||||
{ {1.23f, 0.24f}, {0.24f, 0.25f}, {0.26f, 0.27f}, {2.27f, 0.28f},
|
{ {1.23f, 0.24f}, {0.24f, 0.25f}, {0.26f, 0.27f}, {2.27f, 0.28f},
|
||||||
{0.27f, 0.29f}, {0.37f, 0.39f}, {-0.47f, 0.49f}, {0.57f, 0.59f} };
|
{0.27f, 0.29f}, {0.37f, 0.39f}, {-0.47f, 0.49f}, {0.57f, 0.59f} };
|
||||||
@ -632,9 +628,9 @@ class QuantileHistMock : public QuantileHistMaker {
|
|||||||
RegTree tree = RegTree();
|
RegTree tree = RegTree();
|
||||||
tree.param.UpdateAllowUnknown(cfg_);
|
tree.param.UpdateAllowUnknown(cfg_);
|
||||||
if (double_builder_) {
|
if (double_builder_) {
|
||||||
double_builder_->TestEvaluateSplit(gmatb_, tree);
|
double_builder_->TestEvaluateSplit(tree);
|
||||||
} else {
|
} else {
|
||||||
float_builder_->TestEvaluateSplit(gmatb_, tree);
|
float_builder_->TestEvaluateSplit(tree);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -642,9 +638,9 @@ class QuantileHistMock : public QuantileHistMaker {
|
|||||||
RegTree tree = RegTree();
|
RegTree tree = RegTree();
|
||||||
tree.param.UpdateAllowUnknown(cfg_);
|
tree.param.UpdateAllowUnknown(cfg_);
|
||||||
if (double_builder_) {
|
if (double_builder_) {
|
||||||
double_builder_->TestApplySplit(gmatb_, tree);
|
double_builder_->TestApplySplit(tree);
|
||||||
} else {
|
} else {
|
||||||
float_builder_->TestEvaluateSplit(gmatb_, tree);
|
float_builder_->TestEvaluateSplit(tree);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -714,8 +710,7 @@ TEST(QuantileHist, DistributedSyncHistograms) {
|
|||||||
TEST(QuantileHist, BuildHist) {
|
TEST(QuantileHist, BuildHist) {
|
||||||
// Don't enable feature grouping
|
// Don't enable feature grouping
|
||||||
std::vector<std::pair<std::string, std::string>> cfg
|
std::vector<std::pair<std::string, std::string>> cfg
|
||||||
{{"num_feature", std::to_string(QuantileHistMock::GetNumColumns())},
|
{{"num_feature", std::to_string(QuantileHistMock::GetNumColumns())}};
|
||||||
{"enable_feature_grouping", std::to_string(0)}};
|
|
||||||
QuantileHistMock maker(cfg);
|
QuantileHistMock maker(cfg);
|
||||||
maker.TestBuildHist();
|
maker.TestBuildHist();
|
||||||
const bool single_precision_histogram = true;
|
const bool single_precision_histogram = true;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user