Unify the hist tree method for different devices. (#9363)

This commit is contained in:
Jiaming Yuan
2023-07-11 10:04:39 +08:00
committed by GitHub
parent 20c52f07d2
commit 97ed944209
8 changed files with 242 additions and 142 deletions

View File

@@ -4,11 +4,13 @@
#include <gtest/gtest.h>
#include <xgboost/context.h>
#include <xgboost/host_device_vector.h> // for HostDeviceVector
#include <xgboost/json.h> // for Json, Object
#include <xgboost/learner.h> // for Learner
#include <limits> // for numeric_limits
#include <memory> // for shared_ptr
#include <string> // for string
#include <limits> // for numeric_limits
#include <memory> // for shared_ptr
#include <optional> // for optional
#include <string> // for string
#include "../../../src/data/proxy_dmatrix.h" // for DMatrixProxy
#include "../../../src/gbm/gbtree.h"
@@ -165,6 +167,115 @@ TEST(GBTree, ChoosePredictor) {
// data is not pulled back into host
ASSERT_FALSE(data.HostCanWrite());
}
TEST(GBTree, ChooseTreeMethod) {
bst_row_t n_samples{128};
bst_feature_t n_features{64};
auto Xy = RandomDataGenerator{n_samples, n_features, 0.5f}.GenerateDMatrix(true);
auto with_update = [&](std::optional<std::string> device,
std::optional<std::string> tree_method) {
auto learner = std::unique_ptr<Learner>(Learner::Create({Xy}));
if (tree_method.has_value()) {
learner->SetParam("tree_method", tree_method.value());
}
if (device.has_value()) {
learner->SetParam("gpu_id", device.value());
}
learner->Configure();
for (std::int32_t i = 0; i < 3; ++i) {
learner->UpdateOneIter(0, Xy);
}
Json config{Object{}};
learner->SaveConfig(&config);
auto updater = config["learner"]["gradient_booster"]["updater"];
CHECK(!IsA<Null>(updater));
return updater;
};
auto with_boost = [&](std::optional<std::string> device, std::optional<std::string> tree_method) {
auto learner = std::unique_ptr<Learner>(Learner::Create({Xy}));
if (tree_method.has_value()) {
learner->SetParam("tree_method", tree_method.value());
}
if (device.has_value()) {
learner->SetParam("gpu_id", device.value());
}
learner->Configure();
for (std::int32_t i = 0; i < 3; ++i) {
HostDeviceVector<GradientPair> gpair{GenerateRandomGradients(Xy->Info().num_row_)};
learner->BoostOneIter(0, Xy, &gpair);
}
Json config{Object{}};
learner->SaveConfig(&config);
auto updater = config["learner"]["gradient_booster"]["updater"];
return updater;
};
// | | hist | gpu_hist | exact | NA |
// |--------+---------+----------+-------+-----|
// | CUDA:0 | GPU | GPU (w) | Err | GPU | # not yet tested
// | CPU | CPU | Err | CPU | CPU | # not yet tested
// |--------+---------+----------+-------+-----|
// | -1 | CPU | GPU (w) | CPU | CPU |
// | 0 | GPU | GPU (w) | Err | GPU |
// | NA | CPU | GPU (w) | CPU | CPU |
//
// - (w): warning
// - CPU: Run on CPU.
// - GPU: Run on CUDA.
// - Err: Not feasible.
// - NA: Parameter is not specified.
// When GPU hist is specified with a CPU context, we should emit an error. However, it's
// quite difficult to detect whether the CPU context is being used because it's the
// default or because it's specified by the user.
std::map<std::pair<std::optional<std::string>, std::optional<std::string>>, std::string>
expectation{
// hist
{{"hist", "-1"}, "grow_quantile_histmaker"},
{{"hist", "0"}, "grow_gpu_hist"},
{{"hist", std::nullopt}, "grow_quantile_histmaker"},
// gpu_hist
{{"gpu_hist", "-1"}, "grow_gpu_hist"},
{{"gpu_hist", "0"}, "grow_gpu_hist"},
{{"gpu_hist", std::nullopt}, "grow_gpu_hist"},
// exact
{{"exact", "-1"}, "grow_colmaker,prune"},
{{"exact", "0"}, "err"},
{{"exact", std::nullopt}, "grow_colmaker,prune"},
// NA
{{std::nullopt, "-1"}, "grow_quantile_histmaker"},
{{std::nullopt, "0"}, "grow_gpu_hist"}, // default to hist
{{std::nullopt, std::nullopt}, "grow_quantile_histmaker"},
};
auto run_test = [&](auto fn) {
for (auto const& kv : expectation) {
auto device = kv.first.second;
auto tm = kv.first.first;
if (kv.second == "err") {
ASSERT_THROW({ fn(device, tm); }, dmlc::Error)
<< " device:" << device.value_or("NA") << " tm:" << tm.value_or("NA");
continue;
}
auto up = fn(device, tm);
auto ups = get<Array const>(up);
auto exp_names = common::Split(kv.second, ',');
ASSERT_EQ(exp_names.size(), ups.size());
for (std::size_t i = 0; i < exp_names.size(); ++i) {
ASSERT_EQ(get<String const>(ups[i]["name"]), exp_names[i])
<< " device:" << device.value_or("NA") << " tm:" << tm.value_or("NA");
}
}
};
run_test(with_update);
run_test(with_boost);
}
#endif // XGBOOST_USE_CUDA
// Some other parts of test are in `Tree.JsonIO'.

View File

@@ -57,12 +57,12 @@ class TestTrainingContinuation:
gbdt_02 = xgb.train(xgb_params_01, dtrain_2class,
num_boost_round=0)
gbdt_02.save_model('xgb_tc.model')
gbdt_02.save_model('xgb_tc.json')
gbdt_02a = xgb.train(xgb_params_01, dtrain_2class,
num_boost_round=10, xgb_model=gbdt_02)
gbdt_02b = xgb.train(xgb_params_01, dtrain_2class,
num_boost_round=10, xgb_model="xgb_tc.model")
num_boost_round=10, xgb_model="xgb_tc.json")
ntrees_02a = len(gbdt_02a.get_dump())
ntrees_02b = len(gbdt_02b.get_dump())
assert ntrees_02a == 10
@@ -78,18 +78,18 @@ class TestTrainingContinuation:
gbdt_03 = xgb.train(xgb_params_01, dtrain_2class,
num_boost_round=3)
gbdt_03.save_model('xgb_tc.model')
gbdt_03.save_model('xgb_tc.json')
gbdt_03a = xgb.train(xgb_params_01, dtrain_2class,
num_boost_round=7, xgb_model=gbdt_03)
gbdt_03b = xgb.train(xgb_params_01, dtrain_2class,
num_boost_round=7, xgb_model="xgb_tc.model")
num_boost_round=7, xgb_model="xgb_tc.json")
ntrees_03a = len(gbdt_03a.get_dump())
ntrees_03b = len(gbdt_03b.get_dump())
assert ntrees_03a == 10
assert ntrees_03b == 10
os.remove('xgb_tc.model')
os.remove('xgb_tc.json')
res1 = mean_squared_error(y_2class, gbdt_03a.predict(dtrain_2class))
res2 = mean_squared_error(y_2class, gbdt_03b.predict(dtrain_2class))