xgboost_lib-sys 3.0.5

Native bindings to the xgboost library
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
/**
 * Copyright 2023 by XGBoost contributors
 */
#ifndef XGBOOST_COMMON_RANKING_UTILS_H_
#define XGBOOST_COMMON_RANKING_UTILS_H_
#include <algorithm>                     // for min
#include <cmath>                         // for log2, fabs, floor
#include <cstddef>                       // for size_t
#include <cstdint>                       // for uint32_t, uint8_t, int32_t
#include <limits>                        // for numeric_limits
#include <string>                        // for char_traits, string
#include <vector>                        // for vector

#include "dmlc/parameter.h"              // for FieldEntry, DMLC_DECLARE_FIELD
#include "error_msg.h"                   // for GroupWeight, GroupSize, InvalidCUDAOrdinal
#include "xgboost/base.h"                // for XGBOOST_DEVICE, bst_group_t
#include "xgboost/context.h"             // for Context
#include "xgboost/data.h"                // for MetaInfo
#include "xgboost/host_device_vector.h"  // for HostDeviceVector
#include "xgboost/linalg.h"              // for Vector, VectorView, Tensor
#include "xgboost/logging.h"             // for CHECK_EQ, CHECK
#include "xgboost/parameter.h"           // for XGBoostParameter
#include "xgboost/span.h"                // for Span
#include "xgboost/string_view.h"         // for StringView

namespace xgboost::ltr {
/**
 * \brief Relevance degree
 */
using rel_degree_t = std::uint32_t;  // NOLINT
/**
 * \brief top-k position
 */
using position_t = std::uint32_t;  // NOLINT

/**
 * \brief Maximum relevance degree for NDCG
 */
constexpr std::size_t MaxRel() { return sizeof(rel_degree_t) * 8 - 1; }
static_assert(MaxRel() == 31);

XGBOOST_DEVICE inline double CalcDCGGain(rel_degree_t label) {
  return static_cast<double>((1u << label) - 1);
}

XGBOOST_DEVICE inline double CalcDCGDiscount(std::size_t idx) {
  return 1.0 / std::log2(static_cast<double>(idx) + 2.0);
}

XGBOOST_DEVICE inline double CalcInvIDCG(double idcg) {
  auto inv_idcg = (idcg == 0.0 ? 0.0 : (1.0 / idcg));  // handle irrelevant document
  return inv_idcg;
}

enum class PairMethod : std::int32_t {
  kTopK = 0,
  kMean = 1,
};
}  // namespace xgboost::ltr

DECLARE_FIELD_ENUM_CLASS(xgboost::ltr::PairMethod);

namespace xgboost::ltr {
struct LambdaRankParam : public XGBoostParameter<LambdaRankParam> {
 private:
  static constexpr position_t DefaultK() { return 32; }
  static constexpr position_t DefaultSamplePairs() { return 1; }

 protected:
  // pairs
  // should be accessed by getter for auto configuration.
  // nolint so that we can keep the string name.
  PairMethod lambdarank_pair_method{PairMethod::kTopK};  // NOLINT
  std::size_t lambdarank_num_pair_per_sample{NotSet()};  // NOLINT

 public:
  static constexpr position_t NotSet() { return std::numeric_limits<position_t>::max(); }

  // unbiased
  bool lambdarank_unbiased{false};
  bool lambdarank_normalization{true};
  bool lambdarank_score_normalization{true};
  double lambdarank_bias_norm{1.0};
  // ndcg
  bool ndcg_exp_gain{true};

  bool operator==(LambdaRankParam const& that) const {
    return lambdarank_pair_method == that.lambdarank_pair_method &&
           lambdarank_num_pair_per_sample == that.lambdarank_num_pair_per_sample &&
           lambdarank_unbiased == that.lambdarank_unbiased &&
           lambdarank_normalization == that.lambdarank_normalization &&
           lambdarank_score_normalization == that.lambdarank_score_normalization &&
           lambdarank_bias_norm == that.lambdarank_bias_norm && ndcg_exp_gain == that.ndcg_exp_gain;
  }
  bool operator!=(LambdaRankParam const& that) const { return !(*this == that); }

  [[nodiscard]] double Regularizer() const { return 1.0 / (1.0 + this->lambdarank_bias_norm); }

  /**
   * \brief Get number of pairs for each sample
   */
  [[nodiscard]] position_t NumPair() const {
    if (lambdarank_num_pair_per_sample == NotSet()) {
      switch (lambdarank_pair_method) {
        case PairMethod::kMean:
          return DefaultSamplePairs();
        case PairMethod::kTopK:
          return DefaultK();
      }
    } else {
      return lambdarank_num_pair_per_sample;
    }
    LOG(FATAL) << "Unreachable.";
    return 0;
  }

  [[nodiscard]] bool HasTruncation() const { return lambdarank_pair_method == PairMethod::kTopK; }
  [[nodiscard]] bool IsMean() const { return lambdarank_pair_method == PairMethod::kMean; }

  // Used for evaluation metric and cache initialization, iterate through top-k or the whole list
  [[nodiscard]] auto TopK() const {
    if (HasTruncation()) {
      return NumPair();
    } else {
      return NotSet();
    }
  }

  DMLC_DECLARE_PARAMETER(LambdaRankParam) {
    DMLC_DECLARE_FIELD(lambdarank_pair_method)
        .set_default(PairMethod::kTopK)
        .add_enum("mean", PairMethod::kMean)
        .add_enum("topk", PairMethod::kTopK)
        .describe("Method for constructing pairs.");
    DMLC_DECLARE_FIELD(lambdarank_num_pair_per_sample)
        .set_default(NotSet())
        .set_lower_bound(1)
        .describe("Number of pairs for each sample in the list.");
    DMLC_DECLARE_FIELD(lambdarank_unbiased)
        .set_default(false)
        .describe("Unbiased lambda mart. Use extended IPW to debias click position");
    DMLC_DECLARE_FIELD(lambdarank_normalization)
        .set_default(true)
        .describe("Whether to normalize the leaf value for lambda rank.");
    DMLC_DECLARE_FIELD(lambdarank_score_normalization)
        .set_default(true)
        .describe("Whether to normalize the delta by prediction score difference.");
    DMLC_DECLARE_FIELD(lambdarank_bias_norm)
        .set_default(1.0)
        .set_lower_bound(0.0)
        .describe("Lp regularization for unbiased lambdarank.");
    DMLC_DECLARE_FIELD(ndcg_exp_gain)
        .set_default(true)
        .describe("When set to true, the label gain is 2^rel - 1, otherwise it's rel.");
  }
};

/**
 * \brief Common cached items for ranking tasks.
 */
class RankingCache {
 private:
  void InitOnCPU(Context const* ctx, MetaInfo const& info);
  void InitOnCUDA(Context const* ctx, MetaInfo const& info);
  // Cached parameter
  LambdaRankParam param_;
  // offset to data groups.
  HostDeviceVector<bst_group_t> group_ptr_;
  // store the sorted index of prediction.
  HostDeviceVector<std::size_t> sorted_idx_cache_;
  // Maximum size of group
  std::size_t max_group_size_{0};
  // Normalization for weight
  double weight_norm_{1.0};
  /**
   * CUDA cache
   */
  // offset to threads assigned to each group for gradient calculation
  HostDeviceVector<std::size_t> threads_group_ptr_;
  // Sorted index of label for finding buckets.
  HostDeviceVector<std::size_t> y_sorted_idx_cache_;
  // Cached labels sorted by the model
  HostDeviceVector<float> y_ranked_by_model_;
  // Rounding factor for CUDA deterministic floating point summation. One rounding factor
  // for each ranking group.
  linalg::Vector<GradientPair> roundings_;
  // rounding factor for cost
  HostDeviceVector<double> cost_rounding_;
  // temporary storage for creating rounding factors. Stored as byte to avoid having cuda
  // data structure in here.
  HostDeviceVector<std::uint8_t> max_lambdas_;
  // total number of cuda threads used for gradient calculation
  std::size_t n_cuda_threads_{0};

  // Create model rank list on GPU
  common::Span<std::size_t const> MakeRankOnCUDA(Context const* ctx,
                                                 common::Span<float const> predt);
  // Create model rank list on CPU
  common::Span<std::size_t const> MakeRankOnCPU(Context const* ctx,
                                                common::Span<float const> predt);

 protected:
  [[nodiscard]] std::size_t MaxGroupSize() const { return max_group_size_; }

 public:
  RankingCache(Context const* ctx, MetaInfo const& info, LambdaRankParam const& p) : param_{p} {
    CHECK(param_.GetInitialised());
    if (!info.group_ptr_.empty()) {
      CHECK_EQ(info.group_ptr_.back(), info.labels.Size())
          << error::GroupSize() << "the size of label.";
    }
    if (ctx->IsCUDA()) {
      this->InitOnCUDA(ctx, info);
    } else {
      this->InitOnCPU(ctx, info);
    }
    if (!info.weights_.Empty()) {
      CHECK_EQ(Groups(), info.weights_.Size()) << error::GroupWeight();
    }
    if (param_.HasTruncation()) {
      CHECK_GE(param_.NumPair(), 1);
    }
  }
  [[nodiscard]] std::size_t MaxPositionSize() const {
    // Use truncation level as bound.
    if (param_.HasTruncation()) {
      return param_.NumPair();
    }
    // Hardcoded maximum size of positions to track. We don't need too many of them as the
    // bias decreases exponentially.
    return std::min(max_group_size_, static_cast<std::size_t>(32));
  }
  // Constructed as [1, n_samples] if group ptr is not supplied by the user
  common::Span<bst_group_t const> DataGroupPtr(Context const* ctx) const {
    group_ptr_.SetDevice(ctx->Device());
    return ctx->IsCUDA() ? group_ptr_.ConstDeviceSpan() : group_ptr_.ConstHostSpan();
  }

  [[nodiscard]] auto const& Param() const { return param_; }
  [[nodiscard]] std::size_t Groups() const { return group_ptr_.Size() - 1; }
  [[nodiscard]] double WeightNorm() const { return weight_norm_; }

  // Create a rank list by model prediction
  common::Span<std::size_t const> SortedIdx(Context const* ctx, common::Span<float const> predt) {
    if (sorted_idx_cache_.Empty()) {
      sorted_idx_cache_.SetDevice(ctx->Device());
      sorted_idx_cache_.Resize(predt.size());
    }
    if (ctx->IsCUDA()) {
      return this->MakeRankOnCUDA(ctx, predt);
    } else {
      return this->MakeRankOnCPU(ctx, predt);
    }
  }
  // The function simply returns a uninitialized buffer as this is only used by the
  // objective for creating pairs.
  common::Span<std::size_t> SortedIdxY(Context const* ctx, std::size_t n_samples) {
    CHECK(ctx->IsCUDA()) << error::InvalidCUDAOrdinal();
    if (y_sorted_idx_cache_.Empty()) {
      y_sorted_idx_cache_.SetDevice(ctx->Device());
      y_sorted_idx_cache_.Resize(n_samples);
    }
    return y_sorted_idx_cache_.DeviceSpan();
  }
  common::Span<float> RankedY(Context const* ctx, std::size_t n_samples) {
    CHECK(ctx->IsCUDA()) << error::InvalidCUDAOrdinal();
    if (y_ranked_by_model_.Empty()) {
      y_ranked_by_model_.SetDevice(ctx->Device());
      y_ranked_by_model_.Resize(n_samples);
    }
    return y_ranked_by_model_.DeviceSpan();
  }

  // CUDA cache getters, the cache is shared between metric and objective, some of these
  // fields are initialized lazily to avoid unnecessary allocation.
  [[nodiscard]] common::Span<std::size_t const> CUDAThreadsGroupPtr() const {
    CHECK(!threads_group_ptr_.Empty());
    return threads_group_ptr_.ConstDeviceSpan();
  }
  [[nodiscard]] std::size_t CUDAThreads() const { return n_cuda_threads_; }

  [[nodiscard]] linalg::VectorView<GradientPair> CUDARounding(Context const* ctx) {
    if (roundings_.Size() == 0) {
      roundings_.SetDevice(ctx->Device());
      roundings_.Reshape(Groups());
    }
    return roundings_.View(ctx->Device());
  }
  [[nodiscard]] common::Span<double> CUDACostRounding(Context const* ctx) {
    if (cost_rounding_.Size() == 0) {
      cost_rounding_.SetDevice(ctx->Device());
      cost_rounding_.Resize(1);
    }
    return cost_rounding_.DeviceSpan();
  }
  template <typename Type>
  common::Span<Type> MaxLambdas(Context const* ctx, std::size_t n) {
    max_lambdas_.SetDevice(ctx->Device());
    std::size_t bytes = n * sizeof(Type);
    if (bytes != max_lambdas_.Size()) {
      max_lambdas_.Resize(bytes);
    }
    return common::Span<Type>{reinterpret_cast<Type*>(max_lambdas_.DevicePointer()), n};
  }
};

class NDCGCache : public RankingCache {
  // NDCG discount
  HostDeviceVector<double> discounts_;
  // 1.0 / IDCG
  linalg::Vector<double> inv_idcg_;
  /**
   * CUDA cache
   */
  // store the intermediate DCG calculation result for metric
  linalg::Vector<double> dcg_;

 public:
  void InitOnCPU(Context const* ctx, MetaInfo const& info);
  void InitOnCUDA(Context const* ctx, MetaInfo const& info);

 public:
  NDCGCache(Context const* ctx, MetaInfo const& info, LambdaRankParam const& p)
      : RankingCache{ctx, info, p} {
    if (ctx->IsCUDA()) {
      this->InitOnCUDA(ctx, info);
    } else {
      this->InitOnCPU(ctx, info);
    }
  }

  linalg::VectorView<double const> InvIDCG(Context const* ctx) const {
  // This function doesn't have sycl-specific implementation yet.
  // For that reason we transfer data to host in case of sycl is used for propper execution.
    return inv_idcg_.View(ctx->Device().IsSycl() ? DeviceOrd::CPU() : ctx->Device());
  }
  common::Span<double const> Discount(Context const* ctx) const {
    return ctx->IsCUDA() ? discounts_.ConstDeviceSpan() : discounts_.ConstHostSpan();
  }
  linalg::VectorView<double> Dcg(Context const* ctx) {
    if (dcg_.Size() == 0) {
      dcg_.SetDevice(ctx->Device());
      dcg_.Reshape(this->Groups());
    }
    return dcg_.View(ctx->Device().IsSycl() ? DeviceOrd::CPU() : ctx->Device());
  }
};

/**
 * \brief Validate label for NDCG
 *
 * \tparam NoneOf Implementation of std::none_of. Specified as a parameter to reuse the
 *                check for both CPU and GPU.
 */
template <typename NoneOf>
void CheckNDCGLabels(ltr::LambdaRankParam const& p, linalg::VectorView<float const> labels,
                     NoneOf none_of) {
  auto d_labels = labels.Values();
  if (p.ndcg_exp_gain) {
    auto label_is_integer =
        none_of(d_labels.data(), d_labels.data() + d_labels.size(), [] XGBOOST_DEVICE(float v) {
          auto l = std::floor(v);
          return std::fabs(l - v) > kRtEps || v < 0.0f;
        });
    CHECK(label_is_integer)
        << "When using relevance degree as target, label must be either 0 or positive integer.";
  }

  if (p.ndcg_exp_gain) {
    auto label_is_valid = none_of(d_labels.data(), d_labels.data() + d_labels.size(),
                                  [] XGBOOST_DEVICE(ltr::rel_degree_t v) { return v > MaxRel(); });
    CHECK(label_is_valid) << "Relevance degress must be lesser than or equal to " << MaxRel()
                          << " when the exponential NDCG gain function is used. "
                          << "Set `ndcg_exp_gain` to false to use custom DCG gain.";
  }
}

template <typename AllOf>
bool IsBinaryRel(linalg::VectorView<float const> label, AllOf all_of) {
  auto s_label = label.Values();
  return all_of(s_label.data(), s_label.data() + s_label.size(), [] XGBOOST_DEVICE(float y) {
    return std::abs(y - 1.0f) < kRtEps || std::abs(y - 0.0f) < kRtEps;
  });
}
/**
 * \brief Validate label for precision-based metric.
 *
 * \tparam Implementation of std::all_of. Specified as a parameter to reuse the check for
 *         both CPU and GPU.
 */
template <typename AllOf>
void CheckPreLabels(StringView name, linalg::VectorView<float const> label, AllOf all_of) {
  auto is_binary = IsBinaryRel(label, all_of);
  CHECK(is_binary) << name << " can only be used with binary labels.";
}

class PreCache : public RankingCache {
  HostDeviceVector<double> pre_;

  void InitOnCPU(Context const* ctx, MetaInfo const& info);
  void InitOnCUDA(Context const* ctx, MetaInfo const& info);

 public:
  PreCache(Context const* ctx, MetaInfo const& info, LambdaRankParam const& p)
      : RankingCache{ctx, info, p} {
    if (ctx->IsCUDA()) {
      this->InitOnCUDA(ctx, info);
    } else {
      this->InitOnCPU(ctx, info);
    }
  }

  common::Span<double> Pre(Context const* ctx) {
    if (pre_.Empty()) {
      pre_.SetDevice(ctx->Device());
      pre_.Resize(this->Groups());
    }
    return ctx->IsCUDA() ? pre_.DeviceSpan() : pre_.HostSpan();
  }
};

class MAPCache : public RankingCache {
  // Total number of relevant documents for each group
  HostDeviceVector<double> n_rel_;
  // \sum l_k/k
  HostDeviceVector<double> acc_;
  HostDeviceVector<double> map_;
  // Number of samples in this dataset.
  std::size_t n_samples_{0};

  void InitOnCPU(Context const* ctx, MetaInfo const& info);
  void InitOnCUDA(Context const* ctx, MetaInfo const& info);

 public:
  MAPCache(Context const* ctx, MetaInfo const& info, LambdaRankParam const& p)
      : RankingCache{ctx, info, p}, n_samples_{static_cast<std::size_t>(info.num_row_)} {
    if (ctx->IsCUDA()) {
      this->InitOnCUDA(ctx, info);
    } else {
      this->InitOnCPU(ctx, info);
    }
  }

  common::Span<double> NumRelevant(Context const* ctx) {
    if (n_rel_.Empty()) {
      n_rel_.SetDevice(ctx->Device());
      n_rel_.Resize(n_samples_);
    }
    return ctx->IsCUDA() ? n_rel_.DeviceSpan() : n_rel_.HostSpan();
  }
  common::Span<double> Acc(Context const* ctx) {
    if (acc_.Empty()) {
      acc_.SetDevice(ctx->Device());
      acc_.Resize(n_samples_);
    }
    return ctx->IsCUDA() ? acc_.DeviceSpan() : acc_.HostSpan();
  }
  common::Span<double> Map(Context const* ctx) {
    if (map_.Empty()) {
      map_.SetDevice(ctx->Device());
      map_.Resize(this->Groups());
    }
    return ctx->IsCUDA() ? map_.DeviceSpan() : map_.HostSpan();
  }
};

/**
 * \brief Parse name for ranking metric given parameters.
 *
 * \param [in] name   Null terminated string for metric name
 * \param [in] param  Null terminated string for parameter like the `3-` in `ndcg@3-`.
 * \param [out] topn  Top n documents parsed from param. Unchanged if it's not specified.
 * \param [out] minus Whether we should turn the score into loss. Unchanged if it's not
 *                    specified.
 *
 * \return The name of the metric.
 */
std::string ParseMetricName(StringView name, StringView param, position_t* topn, bool* minus);

/**
 * \brief Parse name for ranking metric given parameters.
 */
std::string MakeMetricName(StringView name, position_t topn, bool minus);
}  // namespace xgboost::ltr
#endif  // XGBOOST_COMMON_RANKING_UTILS_H_