Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions src/common/hist_util.cu
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,9 @@
#include "xgboost/host_device_vector.h"

namespace xgboost::common {
constexpr float SketchContainer::kFactor;

namespace detail {
size_t RequiredSampleCutsPerColumn(int max_bins, size_t num_rows) {
double eps = 1.0 / (WQSketch::kFactor * max_bins);
double eps = SketchEpsilon(max_bins, num_rows);
size_t num_cuts = WQuantileSketch::LimitSizeLevel(num_rows, eps);
return std::min(num_cuts, num_rows);
}
Expand Down
4 changes: 1 addition & 3 deletions src/common/quantile.cuh
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ struct SketchUnique {
*/
class SketchContainer {
public:
static constexpr float kFactor = WQSketch::kFactor;
using OffsetT = bst_idx_t;
static_assert(sizeof(OffsetT) == sizeof(size_t), "Wrong type for sketch element offset.");

Expand All @@ -67,8 +66,7 @@ class SketchContainer {
entries_.resize(n_entries);
}
[[nodiscard]] std::size_t IntermediateNumCuts() const {
auto const base = static_cast<std::size_t>(num_bins_) * kFactor;
auto const eps = 1.0 / static_cast<double>(base);
auto const eps = SketchEpsilon(num_bins_, std::max<std::size_t>(1, rows_seen_));
auto const per_feature = WQSketch::LimitSizeLevel(std::max<std::size_t>(1, rows_seen_), eps);
return per_feature * num_columns_;
}
Expand Down
25 changes: 13 additions & 12 deletions src/common/quantile.h
Original file line number Diff line number Diff line change
Expand Up @@ -557,10 +557,9 @@ struct WQSummaryContainer : public WQSummary<> {
/*! \brief Weighted quantile sketch algorithm using merge/prune. */
class WQuantileSketch {
public:
// Sketch epsilon is approximately `1 / (kFactor * max_bin)` once `max_bin` limits the budget.
// Our current cut-rank measurements suggest an empirical constant of about 2 for the final
// emitted cuts, so the observed normalized cut error is about `2 / kFactor`. With
// `kFactor = 8`, that is roughly `0.25` bins of rank mass, i.e. about a quarter-bin offset.
// Safety factor used to oversample the internal sketch relative to the target rank
// resolution. User-facing epsilon remains the target rank guarantee; `kFactor`
// only affects how much summary storage we reserve to achieve it.
static float constexpr kFactor = 8.0;

public:
Expand All @@ -582,10 +581,11 @@ class WQuantileSketch {
// Empty columns can appear in distributed column-split settings.
return 1;
}
auto const internal_eps = eps / kFactor;
size_t nlevel = 1;
size_t limit_size = 1;
while (true) {
limit_size = static_cast<size_t>(ceil(nlevel / eps)) + 1;
limit_size = static_cast<size_t>(ceil(nlevel / internal_eps)) + 1;
limit_size = std::min(maxn, limit_size);
size_t n = (1ULL << nlevel);
if (n * limit_size >= maxn) break;
Expand All @@ -594,7 +594,8 @@ class WQuantileSketch {
// check invariant
size_t n = (1ULL << nlevel);
CHECK(n * limit_size >= maxn) << "invalid init parameter";
CHECK(nlevel <= std::max(static_cast<size_t>(1), static_cast<size_t>(limit_size * eps)))
CHECK(nlevel <=
std::max(static_cast<size_t>(1), static_cast<size_t>(limit_size * internal_eps)))
<< "invalid init parameter";
return limit_size;
}
Expand Down Expand Up @@ -730,17 +731,17 @@ class WQuantileSketch {
size_t num_elements_{0};
};

[[nodiscard]] inline double SketchEpsilon(bst_bin_t max_bins, std::size_t num_elements) {
auto const n = std::max<std::size_t>(1, num_elements);
[[nodiscard]] inline double SketchEpsilon(bst_bin_t max_bins, std::size_t num_samples) {
auto const n = std::max<std::size_t>(1, num_samples);
auto const n_bins = std::min<std::size_t>(static_cast<std::size_t>(max_bins), n);
return 1.0 / (static_cast<double>(n_bins) * WQuantileSketch::kFactor);
return 1.0 / static_cast<double>(n_bins);
}

// Per-feature summary size for a sketch that represents `num_elements`. `num_elements`
// Per-feature summary size for a sketch that represents `num_samples`. `num_samples`
// can be an exact per-feature count or a conservative approximation when a tighter count
// is not available on the current path.
[[nodiscard]] inline std::size_t SketchSummaryBudget(bst_bin_t max_bins, std::size_t num_elements) {
return WQuantileSketch::LimitSizeLevel(num_elements, SketchEpsilon(max_bins, num_elements));
[[nodiscard]] inline std::size_t SketchSummaryBudget(bst_bin_t max_bins, std::size_t num_samples) {
return WQuantileSketch::LimitSizeLevel(num_samples, SketchEpsilon(max_bins, num_samples));
}

namespace detail {
Expand Down
135 changes: 95 additions & 40 deletions tests/cpp/common/test_quantile.cc
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,104 @@
#include "../../../src/common/hist_util.h"
#include "../../../src/data/adapter.h"
#include "../collective/test_worker.h" // for TestDistributedGlobal
#include "test_quantile_helpers.h"
#include "xgboost/context.h"

namespace xgboost::common {
namespace quantile_test {
class QuantileSummaryTest : public ::testing::TestWithParam<SummaryCase> {};
class QuantileSummarySortedTest : public ::testing::TestWithParam<SummaryCase> {};

TEST_P(QuantileSummaryTest, Invariants) {
auto c = GetParam();
auto col = GenerateSummaryColumn(c);
WQuantileSketch sketch{c.rows, SketchEpsilon(c.max_bin, c.rows)};
for (std::size_t i = 0; i < col.values.size(); ++i) {
sketch.Push(col.values[i], col.weights[i]);
}
auto budget = SketchSummaryBudget(c.max_bin, c.rows);
auto summary = sketch.GetSummary(budget);
auto entries = summary.Entries();
auto ref = AggregateReferenceColumn(col);
auto nonzero_samples = NonZeroWeightCount(col);

// An empty sketch should remain empty after finalization.
if (EmptyReference(ref)) {
ASSERT_TRUE(entries.empty()) << "case=" << c.name;
return;
}

// A numerical summary should remain a strictly increasing support set.
ASSERT_FALSE(entries.empty()) << "case=" << c.name;
for (std::size_t i = 1; i < entries.size(); ++i) {
EXPECT_LT(entries[i - 1].value, entries[i].value) << "case=" << c.name;
}

// Large-n anchors should exercise actual compression rather than exact retention.
if (c.rows > static_cast<std::size_t>(c.max_bin) * 8) {
ASSERT_LT(summary.Size(), nonzero_samples)
<< "case=" << c.name << " should exercise sketch compression.";
}

// The summary query rule should satisfy the target rank bound plus the final prune term.
auto total = TotalWeight(ref);
auto max_error = MaxSummaryQueryRankError(summary, ref, c.max_bin);
auto eps = SketchEpsilon(c.max_bin, c.rows);
auto bound = (eps + 1.0 / static_cast<double>(budget)) * total;

EXPECT_LE(max_error, bound) << "case=" << c.name << ", total=" << total << ", budget=" << budget
<< ", eps=" << eps;

// If the target bin count can already represent all distinct values, the summary should
// preserve the exact support instead of approximating it.
if (UniqueValueCount(ref) <= static_cast<std::size_t>(c.max_bin)) {
auto exact_values = ExactValues(ref);
ASSERT_EQ(entries.size(), exact_values.size()) << "case=" << c.name;
for (std::size_t i = 0; i < exact_values.size(); ++i) {
EXPECT_FLOAT_EQ(entries[i].value, exact_values[i]) << "case=" << c.name;
}
}
}

TEST_P(QuantileSummarySortedTest, QueryBound) {
auto c = GetParam();
auto col = GenerateSummaryColumn(c);
WQuantileSketch sketch{c.rows, SketchEpsilon(c.max_bin, c.rows)};
std::vector<::xgboost::Entry> sorted_col;
sorted_col.reserve(col.values.size());
for (std::size_t i = 0; i < col.values.size(); ++i) {
sorted_col.emplace_back(i, col.values[i]);
}
std::sort(sorted_col.begin(), sorted_col.end(), ::xgboost::Entry::CmpValue);
sketch.PushSorted(Span<::xgboost::Entry const>{sorted_col.data(), sorted_col.size()}, col.weights,
c.max_bin);
auto budget = SketchSummaryBudget(c.max_bin, c.rows);
auto summary = sketch.GetSummary(budget);
auto entries = summary.Entries();
auto ref = AggregateReferenceColumn(col);

if (EmptyReference(ref)) {
ASSERT_TRUE(entries.empty()) << "case=" << c.name;
return;
}

auto total = TotalWeight(ref);
auto max_error = MaxSummaryQueryRankError(summary, ref, c.max_bin);
auto eps = SketchEpsilon(c.max_bin, c.rows);
auto bound = (eps + 1.0 / static_cast<double>(budget)) * total;

EXPECT_LE(max_error, bound) << "case=" << c.name << ", total=" << total << ", budget=" << budget
<< ", eps=" << eps;
}

INSTANTIATE_TEST_SUITE_P(Anchors, QuantileSummaryTest, ::testing::ValuesIn(SummaryAnchorCases()),
CaseName);
INSTANTIATE_TEST_SUITE_P(RandomSamples, QuantileSummaryTest,
::testing::ValuesIn(SummaryRandomCases(100)), CaseName);
INSTANTIATE_TEST_SUITE_P(Anchors, QuantileSummarySortedTest,
::testing::ValuesIn(SummaryAnchorCases()), CaseName);
} // namespace quantile_test

TEST(Quantile, LoadBalance) {
size_t constexpr kRows = 1000, kCols = 100;
auto m = RandomDataGenerator{kRows, kCols, 0}.GenerateDMatrix();
Expand Down Expand Up @@ -64,46 +159,6 @@ TEST(Quantile, TrackSketchElementsSorted) {
ASSERT_GT(out.Size(), 0);
ASSERT_EQ(sketch.NumElements(), 3);
}

TEST(Quantile, SetPruneInplace) {
using Summary = WQSummary<>;
using Entry = Summary::Entry;

SimpleLCG lcg;
for (size_t trial = 0; trial < 256; ++trial) {
size_t n = (lcg() % 256) + 1;
size_t max_size = (lcg() % n) + 1;

std::vector<Entry> src_storage(n);
float running_rank = 0.0f;
for (size_t i = 0; i < n; ++i) {
float w = static_cast<float>((lcg() % 7) + 1);
float value = static_cast<float>(i);
src_storage[i] = Entry{running_rank, running_rank + w, w, value};
running_rank += w;
}

std::vector<Entry> ref_storage(n);
Summary src_ref{Span<Entry>{src_storage.data(), src_storage.size()}, n};
Summary out_ref{Span<Entry>{ref_storage.data(), ref_storage.size()}, 0};
out_ref.CopyFrom(src_ref);
out_ref.SetPrune(max_size);

Summary in_place{Span<Entry>{src_storage.data(), src_storage.size()}, n};
in_place.SetPrune(max_size);

ASSERT_EQ(in_place.Size(), out_ref.Size()) << "trial=" << trial;
auto const in_entries = in_place.Entries();
auto const ref_entries = out_ref.Entries();
for (size_t i = 0; i < in_place.Size(); ++i) {
EXPECT_FLOAT_EQ(in_entries[i].rmin, ref_entries[i].rmin) << "trial=" << trial;
EXPECT_FLOAT_EQ(in_entries[i].rmax, ref_entries[i].rmax) << "trial=" << trial;
EXPECT_FLOAT_EQ(in_entries[i].wmin, ref_entries[i].wmin) << "trial=" << trial;
EXPECT_FLOAT_EQ(in_entries[i].value, ref_entries[i].value) << "trial=" << trial;
}
}
}

namespace {
template <bool use_column>
void PushPage(HostSketchContainer* container, SparsePage const& page, MetaInfo const& info,
Expand Down
Loading
Loading