|
3 | 3 | */ |
4 | 4 | #include <gtest/gtest.h> |
5 | 5 | #include <xgboost/host_device_vector.h> |
| 6 | +#include <xgboost/linalg.h> |
6 | 7 | #include <xgboost/tree_updater.h> |
7 | 8 |
|
| 9 | +#include <cmath> |
8 | 10 | #include <cstddef> // for size_t |
| 11 | +#include <cstring> |
| 12 | +#include <memory> |
9 | 13 | #include <string> |
10 | 14 | #include <vector> |
11 | 15 |
|
12 | 16 | #include "../../../src/tree/common_row_partitioner.h" |
13 | 17 | #include "../../../src/tree/hist/expand_entry.h" // for MultiExpandEntry, CPUExpandEntry |
14 | | -#include "../collective/test_worker.h" // for TestDistributedGlobal |
| 18 | +#include "../collective/test_worker.h" // for TestDistributedGlobal |
15 | 19 | #include "../helpers.h" |
16 | 20 | #include "test_column_split.h" // for TestColumnSplit |
17 | 21 | #include "test_partitioner.h" |
18 | 22 | #include "xgboost/data.h" |
| 23 | +#include "xgboost/task.h" |
19 | 24 |
|
20 | 25 | namespace xgboost::tree { |
21 | 26 | namespace { |
@@ -246,4 +251,94 @@ INSTANTIATE_TEST_SUITE_P(ColumnSplit, TestHistColumnSplit, ::testing::ValuesIn([ |
246 | 251 | } |
247 | 252 | return params; |
248 | 253 | }())); |
| 254 | + |
| 255 | +namespace { |
| 256 | +void FillGradients(linalg::Matrix<GradientPair>* gpair) { |
| 257 | + auto h = gpair->HostView(); |
| 258 | + for (std::size_t row = 0; row < h.Shape(0); ++row) { |
| 259 | + for (std::size_t target = 0; target < h.Shape(1); ++target) { |
| 260 | + h(row, target) = GradientPair{1.0f, 0.0f}; |
| 261 | + } |
| 262 | + } |
| 263 | +} |
| 264 | + |
| 265 | +// Verify partitioner doesn't write past buffer end when doing |
| 266 | +// update on small dataset after large one. |
| 267 | +void TestPartitionerOverrun(bst_target_t n_targets) { |
| 268 | + constexpr bst_idx_t kNBig = 1 << 16, kNSmall = 1024; |
| 269 | + constexpr int kCols = 3; |
| 270 | + |
| 271 | + Context ctx; |
| 272 | + ctx.InitAllowUnknown(Args{{"nthread", "1"}}); |
| 273 | + |
| 274 | + ObjInfo task{ObjInfo::kRegression, true, true}; |
| 275 | + auto updater = |
| 276 | + std::unique_ptr<TreeUpdater>{TreeUpdater::Create("grow_quantile_histmaker", &ctx, &task)}; |
| 277 | + |
| 278 | + TrainParam param; |
| 279 | + param.InitAllowUnknown(Args{{"max_depth", "1"}, |
| 280 | + {"max_bin", "32"}, |
| 281 | + {"lambda", "0"}, |
| 282 | + {"gamma", "0"}, |
| 283 | + {"min_child_weight", "0"}}); |
| 284 | + updater->Configure(Args{}); |
| 285 | + |
| 286 | + auto const n_targets_size = static_cast<std::size_t>(n_targets); |
| 287 | + |
| 288 | + auto dmat_large = |
| 289 | + RandomDataGenerator{kNBig, kCols, 0.0f}.Seed(0).Batches(8).GenerateSparsePageDMatrix( |
| 290 | + "part_resize_big_first", true); |
| 291 | + |
| 292 | + std::size_t shape_large[2]{dmat_large->Info().num_row_, n_targets_size}; |
| 293 | + linalg::Matrix<GradientPair> gpair_large(shape_large, ctx.Device()); |
| 294 | + FillGradients(&gpair_large); |
| 295 | + |
| 296 | + RegTree tree_large{n_targets, static_cast<bst_feature_t>(kCols)}; |
| 297 | + std::vector<RegTree*> trees_large{&tree_large}; |
| 298 | + std::vector<HostDeviceVector<bst_node_t>> position_large(1); |
| 299 | + common::Span<HostDeviceVector<bst_node_t>> pos_large{position_large.data(), 1}; |
| 300 | + updater->Update(¶m, &gpair_large, dmat_large.get(), pos_large, trees_large); |
| 301 | + |
| 302 | + auto dmat_small = |
| 303 | + RandomDataGenerator{kNSmall, kCols, 0.0f}.Seed(1).Batches(1).GenerateSparsePageDMatrix( |
| 304 | + "part_resize_small_second", false); |
| 305 | + |
| 306 | + std::vector<HostDeviceVector<bst_node_t>> position_small(1); |
| 307 | + auto& pos = position_small.front(); |
| 308 | + pos.Resize(kNBig); // Allocate large |
| 309 | + pos.Resize(kNSmall); // Shrink logical size, capacity remains large |
| 310 | + |
| 311 | + auto& hv = pos.HostVector(); |
| 312 | + std::size_t cap = hv.capacity(); |
| 313 | + ASSERT_GE(cap, static_cast<std::size_t>(kNBig)); |
| 314 | + |
| 315 | + std::size_t tail_elems = cap - hv.size(); |
| 316 | + ASSERT_GT(tail_elems, 0u) << "Expected reserved tail storage"; |
| 317 | + std::vector<bst_node_t> tail_before(tail_elems); |
| 318 | + std::memcpy(tail_before.data(), hv.data() + hv.size(), tail_elems * sizeof(bst_node_t)); |
| 319 | + |
| 320 | + std::size_t shape_small[2]{dmat_small->Info().num_row_, n_targets_size}; |
| 321 | + linalg::Matrix<GradientPair> gpair_small(shape_small, ctx.Device()); |
| 322 | + FillGradients(&gpair_small); |
| 323 | + |
| 324 | + RegTree tree_small{n_targets, static_cast<bst_feature_t>(kCols)}; |
| 325 | + std::vector<RegTree*> trees_small{&tree_small}; |
| 326 | + common::Span<HostDeviceVector<bst_node_t>> pos_small{position_small.data(), 1}; |
| 327 | + updater->Update(¶m, &gpair_small, dmat_small.get(), pos_small, trees_small); |
| 328 | + |
| 329 | + // Verify no buffer overrun: tail bytes should be unchanged |
| 330 | + ASSERT_EQ(hv.capacity(), cap) << "Test precondition violated: capacity changed"; |
| 331 | + std::vector<bst_node_t> tail_after(tail_elems); |
| 332 | + std::memcpy(tail_after.data(), hv.data() + hv.size(), tail_elems * sizeof(bst_node_t)); |
| 333 | + |
| 334 | + EXPECT_EQ(tail_before, tail_after) |
| 335 | + << "Buffer overrun detected: writes past kNSmall when updating small " |
| 336 | + "single-batch DMatrix after large multi-batch one. " |
| 337 | + "Likely stale partitioner writing to buffer."; |
| 338 | +} |
| 339 | +} // anonymous namespace |
| 340 | + |
| 341 | +TEST(QuantileHist, HistUpdaterPartitionerOverrun) { TestPartitionerOverrun(1); } |
| 342 | + |
| 343 | +TEST(QuantileHist, MultiTargetHistBuilderPartitionerOverrun) { TestPartitionerOverrun(3); } |
249 | 344 | } // namespace xgboost::tree |
0 commit comments