-
-
Notifications
You must be signed in to change notification settings - Fork 25.6k
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
ENH Simplify pytest global random test plugin #27963
ENH Simplify pytest global random test plugin #27963
Conversation
@lesteve Yes I confirm this PR fixes the issue also on my local machine. Thank you very much! |
…nto simplify-pytest-plugin
As asked by @jeremiedbb I double-checked that the following command works (he remembered some issues when working on the original PR):
The only weird behaviours I noticed:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think fixing the "any" case with xdist is important. See below:
sklearn/conftest.py
Outdated
# In these edge cases, random_seeds is set to a fixed value | ||
random_seeds = getattr( | ||
config.workerinput, "random_seeds", default_random_seeds | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does not seem to pickup the seed randomly selected by the controller:
SKLEARN_TESTS_GLOBAL_RANDOM_SEED="any" pytest -n 8 -v sklearn/cluster/tests/test_k_means.py -k test_sample_weight_init
====================================================================================================== test session starts =======================================================================================================
platform darwin -- Python 3.11.6, pytest-7.4.2, pluggy-1.3.0 -- /Users/ogrisel/miniforge3/envs/dev/bin/python3.11
cachedir: .pytest_cache
To reproduce this test run, set the following environment variable:
SKLEARN_TESTS_GLOBAL_RANDOM_SEED="89"
See: https://scikit-learn.org/dev/computing/parallelism.html#sklearn-tests-global-random-seed
rootdir: /Users/ogrisel/code/scikit-learn
configfile: setup.cfg
plugins: repeat-0.9.2, anyio-4.0.0, cov-4.1.0, xdist-3.3.1
8 workers [2 items]
scheduling tests via LoadScheduling
sklearn/cluster/tests/test_k_means.py::test_sample_weight_init[42-random]
sklearn/cluster/tests/test_k_means.py::test_sample_weight_init[42-k-means++]
[gw0] [ 50%] PASSED sklearn/cluster/tests/test_k_means.py::test_sample_weight_init[42-random]
[gw1] [100%] PASSED sklearn/cluster/tests/test_k_means.py::test_sample_weight_init[42-k-means++]
======================================================================================================= 2 passed in 1.69s ========================================================================================================
It's always running the tests with 42 while it mentions 89 in the test report header message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does work as expected when I do not enable xdist though:
====================================================================================================== test session starts =======================================================================================================
platform darwin -- Python 3.11.6, pytest-7.4.2, pluggy-1.3.0 -- /Users/ogrisel/miniforge3/envs/dev/bin/python3.11
cachedir: .pytest_cache
To reproduce this test run, set the following environment variable:
SKLEARN_TESTS_GLOBAL_RANDOM_SEED="82"
See: https://scikit-learn.org/dev/computing/parallelism.html#sklearn-tests-global-random-seed
rootdir: /Users/ogrisel/code/scikit-learn
configfile: setup.cfg
plugins: repeat-0.9.2, anyio-4.0.0, cov-4.1.0, xdist-3.3.1
collected 280 items / 278 deselected / 2 selected
sklearn/cluster/tests/test_k_means.py::test_sample_weight_init[82-k-means++] PASSED [ 50%]
sklearn/cluster/tests/test_k_means.py::test_sample_weight_init[82-random] PASSED [100%]
=============================================================================================== 2 passed, 278 deselected in 0.17s ================================================================================================
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hum this is what you commented above:
as I mentioned in one of the comment pytest --pyargs sklearn.tests.test_dummy -n2 does not seem to run pytest_configure_node so in the 'any' case we set a fixed random_state (the default value). I don't think this matters that much in practice.
I don't understand why you think it does not matter. The "any" mode is used by our nightly CI to discover unexpected seed-sensitive test failures over time. I think it's important.
As I commented above this is not related to the use of --pyargs
: I do not use it in the reproducer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for testing, I thought I tested a variety of combinations:
SKLEARN_TESTS_GLOBAL_RANDOM_SEED='any' pytest sklearn -n2
SKLEARN_TESTS_GLOBAL_RANDOM_SEED='any' pytest --pyargs sklearn -n2
SKLEARN_TESTS_GLOBAL_RANDOM_SEED='any' pytest sklearn/tests/test_dummy.py -n2
SKLEARN_TESTS_GLOBAL_RANDOM_SEED='any' pytest --pyargs sklearn.tests.test_dummy -n2
and only 4. did not work for some unknown reason, which is why I said it is not that important.
Apparently it is more complicated than this, so I need to check again!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I fixed the bug I think. I was using getattr(config.workerinput, ...)
rather than config.workerinput.get
...
I tested the cases mentioned in my previous comment and now only 4. does not work (as expected).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For completeness, I believe the issue is actually pytest-dev/pytest-xdist#917. pytest_configure
is not run in the xdist controller in 4. for some reason so the mechanism to create the random seed in the main controller and access it in the xdist workers does not work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible to generate the seed in each worker instead of having to use the controller?
I am thinking of something like calling the random number generator with a shared seed in each worker. To have a shared seed we need some form of state that is easily accessed by each worker. I don't know if such a thing exists in pytest, maybe each run is assigned some kind of "run ID" or something?
If each worker can derive the correct seed itself we don't need to have to use the controller to coordinate the workers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting idea, I'll look into it!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking about it, you actually want the controller to know about the seed, be it only to display it in pytest_report_header
.
I looked a bit at PYTEST_XDIST_TESTRUNUID
env variable but it is only available in the workers and not the controller, there is the testrun_uid
fixture but I am not sureit can be made to fit nicely with the pytest_generate_tests
with metafunc
thing.
…nto simplify-pytest-plugin
Co-authored-by: Olivier Grisel <olivier.grisel@ensta.org>
…nto simplify-pytest-plugin
Question from Discord by @thomasjpfan:
The only thing that does not quite work as well as SKLEARN_TESTS_GLOBAL_RANDOM_SEED='any' pytest --pyargs sklearn.tests.test_dummy -n2 -v will always use 42 as the random seed. The underlying Pytest issue seems to be pytest-dev/pytest-xdist#917. Apart from using SKLEARN_TESTS_GLOBAL_RANDOM_SEED='1-10' pytest --pyargs sklearn.tests.test_dummy -n2 -v I think the edge case is used seldom enough that it is not such an issue compared to the advantages:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lesteve I agree this PR is a cleaner solution. In the nightly builds, we set SKLEARN_TESTS_GLOBAL_RANDOM_SEED='any'
here:
export SKLEARN_TESTS_GLOBAL_RANDOM_SEED="any" |
which will be used with --pyargs
:
TEST_CMD="$TEST_CMD --pyargs sklearn" |
After this PR is merged, will this seed always be 42?
…nto simplify-pytest-plugin
@thomasjpfan thanks a lot for taking a look 🙏, this PR is definitely not completely trivial to review! I still think it kind of "simplifies" things a bit, but each time I have a new look, it takes me a bit of time to put things back in my short-term memory 😅 ...
With this PR merged, everything will work the same as previously: the CI will pick a random seed each time. For example, this does work locally so I this should work in the CI: SKLEARN_TESTS_GLOBAL_RANDOM_SEED='any' pytest --pyargs sklearn -n2 -k test_median_strategy_regressor -v Here is the ouptut, notice
Full disclosure: if you run the same command outside of the scikit-learn root folder, you will be in the pathological case and get I have never noticed this, but the current behaviour of The error comes from the fact that random seeds are different in the two workers:
|
Let me try to do some kind of summary to make it easier. To be explicit, this is when running from scikit-learn root folder, which I think is 90-99% of our usage.
I would argue the partial tests + Here is an explanation of the different cases:
|
In the CI, we do move out of the root directory: Does this PR work when |
Indeed I forgot about this ...
No it does not 😓, when not running from the scikit-learn root folder, the global random seed will always be 42.
Part of the output (that shows the global random seed is 42):
I am wondering whether it would actually not be a lot simpler to:
For visibility, the underlying pytest-xdist limitation seems to be pytest-dev/pytest-xdist#917. |
In my last commit I implemented what I mentioned in my previous comment. I think by removing the
|
@@ -11,7 +11,7 @@ if [[ "$BUILD_REASON" == "Schedule" ]]; then | |||
# Enable global random seed randomization to discover seed-sensitive tests | |||
# only on nightly builds. | |||
# https://scikit-learn.org/stable/computing/parallelism.html#environment-variables | |||
export SKLEARN_TESTS_GLOBAL_RANDOM_SEED="any" | |||
export SKLEARN_TESTS_GLOBAL_RANDOM_SEED=$(($RANDOM % 100)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this alternative, it's a lot more intuitive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lesteve Can we do a quick "echo" here to show SKLEARN_TESTS_GLOBAL_RANDOM_SEED
in the CI output?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can even move the message of the header here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
sklearn/conftest.py
Outdated
def pytest_report_header(config): | ||
random_seed_var = environ.get("SKLEARN_TESTS_GLOBAL_RANDOM_SEED") | ||
random_seeds = random_seed_var | ||
|
||
return [ | ||
"To reproduce this test run, set the following environment variable:", | ||
f' SKLEARN_TESTS_GLOBAL_RANDOM_SEED="{random_seeds}"', | ||
( | ||
"See: https://scikit-learn.org/dev/computing/parallelism.html" | ||
"#sklearn-tests-global-random-seed" | ||
), | ||
] | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With this PR, the header doesn't show up in the CI runs anymore. I think it has limited value since we always see the seed that was used in the pytest report of failed tests because it's in the parametrisation of the test. Also I don't think that any contributor has ever needed and even less seen this.
So I think it's fine to just remove this header. We can document the pytest command to run for a given seed in parallelism.rst
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. I would even remove the header report because I don't it useful anymore since it now doesn't show up in the CI anymore, but I'm fine with or without it.
Great to see this one merged! It took some time to get there, but as often through feed-back and discussions, we reached a more robust solution in the end! |
Co-authored-by: Olivier Grisel <olivier.grisel@ensta.org> Co-authored-by: Jérémie du Boisberranger <jeremie@probabl.ai>
Reference Issues/PRs
Fix #27806
@Charlie-XIAO can you double-check this fixes it for you? This fixes it in @fcharras Windows VM and mine too, but one more confirmation would be nice.
What does this implement/fix? Explain your changes.
I think we were in a weird edge-case of pytest by combining registering a plugin in the
sklearn
tree calling it via setup.cfg and sometimes registering insideconftest.py
some reason. It seems like by moving all the code toconftest.py
we avoid the issue.pytest_generate_tests seems a slightly simpler way to parameterize the global_random_seeds tests based on the variable SKLEARN_TESTS_GLOBAL_RANDOM_SEED.
cc @jeremiedbb who was involved in the global random seed plugin originally IIRC #22749. Edit: actually looks like @ogrisel was the one who created the global random seed plugin.