Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #3523: Fix CustomGlobalRandomEngine for R #3781

Merged
merged 1 commit into from
Oct 15, 2018
Merged

Conversation

hcho3
Copy link
Collaborator

@hcho3 hcho3 commented Oct 9, 2018

Diagnosis Apple Clang's implementation of std::shuffle doesn't work correctly when it is run with the random bit generator for R package:

CustomGlobalRandomEngine::result_type
CustomGlobalRandomEngine::operator()() {
  return static_cast<result_type>(
      std::floor(unif_rand() * CustomGlobalRandomEngine::max()));
}

Minimial reproduction of failure (compile using Apple Clang 10.0):

std::vector<int> feature_set(100);
std::iota(feature_set.begin(), feature_set.end(), 0);
    // initialize with 0, 1, 2, 3, ..., 99
std::shuffle(feature_set.begin(), feature_set.end(), common::GlobalRandom());
    // This returns 0, 1, 2, ..., 99, so content didn't get shuffled at all!!!

Note that this bug is platform-dependent; it does not appear when GCC or upstream LLVM Clang is used.

Fix Use a platform-independent implementation of std::shuffle. A header-only library called PCG (Apache license) is included for this purpose.

Closes #3523.

TODO: add a regression test, replace other occurrences of std::shuffle in XGBoost codebase

@hcho3
Copy link
Collaborator Author

hcho3 commented Oct 9, 2018

@trivialfis
Copy link
Member

Hi, is other std random method safe to use ?

@hcho3
Copy link
Collaborator Author

hcho3 commented Oct 10, 2018

@trivialfis No idea. One thing I learned is that random functions in std are NOT portable in their behavior on different platforms: https://www.reddit.com/r/cpp/comments/7i21sn/til_uniform_int_distribution_is_not_portable/
In the future, if random functions show undesirable behavior on certain platforms, we'll have to replace them with a custom implementation.

@trivialfis
Copy link
Member

Thanks for the link. The current tests use random to generate dmatrix, see:

std::shared_ptr<xgboost::DMatrix>* CreateDMatrix(int rows, int columns,

It may be helpful to make tests reproducible.

@hcho3
Copy link
Collaborator Author

hcho3 commented Oct 10, 2018

@trivialfis Sure, we can look into using a random generation library. Boost.Random and PCG come to mind. The former increases build complexity greatly, whereas the latter doesn't compile well with MSVC currently. Any other suggestion?

@trivialfis
Copy link
Member

No, this is very new to me. I am currently trying to add unittests for gpu-hist, for now I might just hard code the generated matrix. :(

@hcho3
Copy link
Collaborator Author

hcho3 commented Oct 10, 2018

@trivialfis

I might just hard code the generated matrix. :(

You can do that, but it doesn't look elegant to me. Let me look into Boost.Random. It looks like we don't have to include entire Boost, as Boost.Random comes in a separate repository: https://github.com/boostorg/random
Let me try it. If build is not too painful, I might just include Boost.Random into this PR.

@trivialfis
Copy link
Member

Great! Thanks.

@trivialfis
Copy link
Member

Suggested by @RAMitchell, I implemented a very simple Linear congruential generator in test helper for testing purpose only. This should solve the problem at my hand. I need to modify tests I created for the change of generator before I push it to #3785. Hope that helps. :)

@hcho3
Copy link
Collaborator Author

hcho3 commented Oct 11, 2018

@trivialfis I gave Boost.Random a shot, but Boost modules are so tightly-coupled that Boost.Random ended up requiring 18 Boost modules :( Yes, a simple implementation of linear congruential generator is the way to go, since we can have reproducible tests without pulling insane amount of dependencies.

@hcho3
Copy link
Collaborator Author

hcho3 commented Oct 11, 2018

Also need to mention that the 18 Boost modules amount to 443K lines of code, almost all being headers. This will potentially increase compilation time a lot. Therefore, I motion against including Boost as dependency.

@trivialfis
Copy link
Member

Great. I should push the update tomorrow. :)

@khotilov
Copy link
Member

I wonder if the 64 bit random integers produced by the custom random engine while the apples clang's shuffle somehow expects them to be 32 bit here could be the root cause?

@thvasilo
Copy link
Contributor

It does seem very weird that even such a minimal example wouldn't work, which basically means that std::shuffle is broken for Apple's clang, which is unlikely.

What I see in the code for common::GlobalRandom() is that it does not initialize the Mersenne twister with any seed, and is also not declared as thread-local.

Have you tried initializing the random engine like:

    std::random_device rd;
    std::mt19937 g(rd());

    std::shuffle(v.begin(), v.end(), g);

I would be very surprised if this didn't work on all platforms. That said, the result might still differ between platforms because it's implementation specific, so if we want consistency then PCG seems like the best option.

In general these things are easy to get wrong, I can definitely recommend Stephan Lavavej's talk on the subject: https://channel9.msdn.com/Events/GoingNative/2013/rand-Considered-Harmful
Especially the second part where he talks about the C++11 random engines and how to use them.

@khotilov I think std::mt19937 produces 32-bit numbers, there is std::mt19937_64 for 64-bit.

@hcho3
Copy link
Collaborator Author

hcho3 commented Oct 12, 2018

@thvasilo std::shuffle works well if the Mersenne Twister (MT) is used. The bug being discussed is when the MT is not used. When XGBoost is compiled for R support, the macro XGBOOST_STRICT_R_MODE is enabled and
the custom generator CustomGlobalRandomEngine is used instead of MT. The shuffle function doesn't work when the custom generator is given.

also not declared as thread-local.

The random generator is wrapped in thread local storage: https://github.com/dmlc/xgboost/blob/d594b11f3590981c9c9aa3eaa9233027be312c6a/src/common/common.cc

@hcho3
Copy link
Collaborator Author

hcho3 commented Oct 12, 2018

@khotilov This is a possibility. Let me try changing CustomGlobalRandomEngine to 32-bit.

@thvasilo
Copy link
Contributor

@hcho3 OK, so the problem lies in the CustomGlobalRandomEngine gotcha.

@tqchen
Copy link
Member

tqchen commented Oct 12, 2018

Let us try to look closer at least, it could be very possible due to reason @khotilov suggested and we just have to fix the CustomRandomEngine

@hcho3
Copy link
Collaborator Author

hcho3 commented Oct 12, 2018

@tqchen @khotilov Indeed, changing the return type of CustomGlobalRandomEngine to 32-bit fixes the issue. For now, I'll change the return type only for Apple Clang, so that we don't break anything.

@hcho3
Copy link
Collaborator Author

hcho3 commented Oct 13, 2018

The colsample_bytree test is failing for R + MSVC. Let's see if we'd need to use 32-bit for Windows as well.

@khotilov
Copy link
Member

It looks like R has only 32-bit random generators. E.g., here's the code for its default Mersenne Twister generator: https://github.com/wch/r-source/blob/bc124ff19cca95328e114f0fc2f068150ee2aa61/src/main/RNG.c#L633
So it does not really make much sense to cast it to 64 bit integer. So unless there are significant concerns with reproducibility, let's just use uint32_t everywhere. The xgboost's own default random engine is 32 bit std::mt19937 anyway.

**Symptom** Apple Clang's implementation of `std::shuffle` expects doesn't work
correctly when it is run with the random bit generator for R package:
```cpp
CustomGlobalRandomEngine::result_type
CustomGlobalRandomEngine::operator()() {
  return static_cast<result_type>(
      std::floor(unif_rand() * CustomGlobalRandomEngine::max()));
}
```

Minimial reproduction of failure (compile using Apple Clang 10.0):
```cpp
std::vector<int> feature_set(100);
std::iota(feature_set.begin(), feature_set.end(), 0);
    // initialize with 0, 1, 2, 3, ..., 99
std::shuffle(feature_set.begin(), feature_set.end(), common::GlobalRandom());
    // This returns 0, 1, 2, ..., 99, so content didn't get shuffled at all!!!
```

Note that this bug is platform-dependent; it does not appear when GCC or
upstream LLVM Clang is used.

**Diagnosis** Apple Clang's `std::shuffle` expects 32-bit integer
inputs, whereas `CustomGlobalRandomEngine::operator()` produces 64-bit
integers.

**Fix** Have `CustomGlobalRandomEngine::operator()` produce 32-bit integers.

Closes dmlc#3523.
@hcho3 hcho3 changed the title Fix #3523: Use platform-independent shuffle for colsample Fix #3523: Fix CustomGlobalRandomEngine for R Oct 15, 2018
@hcho3 hcho3 merged commit b38c636 into dmlc:master Oct 15, 2018
@hcho3 hcho3 deleted the fix_colsample branch October 15, 2018 16:39
CodingCat pushed a commit to CodingCat/xgboost that referenced this pull request Oct 25, 2018
**Symptom** Apple Clang's implementation of `std::shuffle` expects doesn't work
correctly when it is run with the random bit generator for R package:
```cpp
CustomGlobalRandomEngine::result_type
CustomGlobalRandomEngine::operator()() {
  return static_cast<result_type>(
      std::floor(unif_rand() * CustomGlobalRandomEngine::max()));
}
```

Minimial reproduction of failure (compile using Apple Clang 10.0):
```cpp
std::vector<int> feature_set(100);
std::iota(feature_set.begin(), feature_set.end(), 0);
    // initialize with 0, 1, 2, 3, ..., 99
std::shuffle(feature_set.begin(), feature_set.end(), common::GlobalRandom());
    // This returns 0, 1, 2, ..., 99, so content didn't get shuffled at all!!!
```

Note that this bug is platform-dependent; it does not appear when GCC or
upstream LLVM Clang is used.

**Diagnosis** Apple Clang's `std::shuffle` expects 32-bit integer
inputs, whereas `CustomGlobalRandomEngine::operator()` produces 64-bit
integers.

**Fix** Have `CustomGlobalRandomEngine::operator()` produce 32-bit integers.

Closes dmlc#3523.
alois-bissuel pushed a commit to criteo-forks/xgboost that referenced this pull request Dec 4, 2018
**Symptom** Apple Clang's implementation of `std::shuffle` expects doesn't work
correctly when it is run with the random bit generator for R package:
```cpp
CustomGlobalRandomEngine::result_type
CustomGlobalRandomEngine::operator()() {
  return static_cast<result_type>(
      std::floor(unif_rand() * CustomGlobalRandomEngine::max()));
}
```

Minimial reproduction of failure (compile using Apple Clang 10.0):
```cpp
std::vector<int> feature_set(100);
std::iota(feature_set.begin(), feature_set.end(), 0);
    // initialize with 0, 1, 2, 3, ..., 99
std::shuffle(feature_set.begin(), feature_set.end(), common::GlobalRandom());
    // This returns 0, 1, 2, ..., 99, so content didn't get shuffled at all!!!
```

Note that this bug is platform-dependent; it does not appear when GCC or
upstream LLVM Clang is used.

**Diagnosis** Apple Clang's `std::shuffle` expects 32-bit integer
inputs, whereas `CustomGlobalRandomEngine::operator()` produces 64-bit
integers.

**Fix** Have `CustomGlobalRandomEngine::operator()` produce 32-bit integers.

Closes dmlc#3523.
@lock lock bot locked as resolved and limited conversation to collaborators Feb 4, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants