groups parameter detected as missing in nested cross-validation with `GridSearchCV` and `cross_val_score`

### Describe the bug

In a nested cross-validation situation with grouped k-fold, the `groups` parameter is being reported as taking the value of `None` despite being provided.

### Steps/Code to Reproduce

``` python 
import numpy as np
from sklearn import model_selection, kernel_ridge

X = np.array([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10], [11, 12]])
y = np.array([1, 2, 3, 4, 5, 6])
groups = np.array([0, 0, 2, 2, 3, 3])
group_kfold = model_selection.GroupKFold(n_splits=2)

krr = kernel_ridge.KernelRidge()
hyper_cv = model_selection.GridSearchCV(
    krr, param_grid={"alpha": [0.1, 0.2]}, cv=group_kfold, scoring="r2"
)
model = hyper_cv.fit(X=X, y=y, groups=groups)
test_score = model_selection.cross_val_score(model, X, y, cv=group_kfold, groups=groups)
```

### Expected Results

No error

### Actual Results

``` python 
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[185], line 11
      7 hyper_cv = model_selection.GridSearchCV(
      8     krr, param_grid={"alpha": [0.1, 0.2]}, cv=group_kfold, scoring="r2"
      9 )
     10 model = hyper_cv.fit(X=X, y=y, groups=groups)
---> 11 test_score = model_selection.cross_val_score(model, X, y, cv=group_kfold, groups=groups)

File ~/git/manuscripts/cost-of-motion/.pixi/envs/default/lib/python3.14/site-packages/sklearn/utils/_param_validation.py:218, in validate_params.<locals>.decorator.<locals>.wrapper(*args, **kwargs)
    212 try:
    213     with config_context(
    214         skip_parameter_validation=(
    215             prefer_skip_nested_validation or global_skip_validation
    216         )
    217     ):
--> 218         return func(*args, **kwargs)
    219 except InvalidParameterError as e:
    220     # When the function is just a wrapper around an estimator, we allow
    221     # the function to delegate validation to the estimator, but we replace
    222     # the name of the estimator by the name of the function in the error
    223     # message to avoid confusion.
    224     msg = re.sub(
    225         r"parameter of \w+ must be",
    226         f"parameter of {func.__qualname__} must be",
    227         str(e),
    228     )

File ~/git/manuscripts/cost-of-motion/.pixi/envs/default/lib/python3.14/site-packages/sklearn/model_selection/_validation.py:677, in cross_val_score(estimator, X, y, groups, scoring, cv, n_jobs, verbose, params, pre_dispatch, error_score)
    674 # To ensure multimetric format is not supported
    675 scorer = check_scoring(estimator, scoring=scoring)
--> 677 cv_results = cross_validate(
    678     estimator=estimator,
    679     X=X,
    680     y=y,
    681     groups=groups,
    682     scoring={"score": scorer},
    683     cv=cv,
    684     n_jobs=n_jobs,
    685     verbose=verbose,
    686     params=params,
    687     pre_dispatch=pre_dispatch,
    688     error_score=error_score,
    689 )
    690 return cv_results["test_score"]

File ~/git/manuscripts/cost-of-motion/.pixi/envs/default/lib/python3.14/site-packages/sklearn/utils/_param_validation.py:218, in validate_params.<locals>.decorator.<locals>.wrapper(*args, **kwargs)
    212 try:
    213     with config_context(
    214         skip_parameter_validation=(
    215             prefer_skip_nested_validation or global_skip_validation
    216         )
    217     ):
--> 218         return func(*args, **kwargs)
    219 except InvalidParameterError as e:
    220     # When the function is just a wrapper around an estimator, we allow
    221     # the function to delegate validation to the estimator, but we replace
    222     # the name of the estimator by the name of the function in the error
    223     # message to avoid confusion.
    224     msg = re.sub(
    225         r"parameter of \w+ must be",
    226         f"parameter of {func.__qualname__} must be",
    227         str(e),
    228     )

File ~/git/manuscripts/cost-of-motion/.pixi/envs/default/lib/python3.14/site-packages/sklearn/model_selection/_validation.py:419, in cross_validate(estimator, X, y, groups, scoring, cv, n_jobs, verbose, params, pre_dispatch, return_train_score, return_estimator, return_indices, error_score)
    398 parallel = Parallel(n_jobs=n_jobs, verbose=verbose, pre_dispatch=pre_dispatch)
    399 results = parallel(
    400     delayed(_fit_and_score)(
    401         clone(estimator),
   (...)    416     for train, test in indices
    417 )
--> 419 _warn_or_raise_about_fit_failures(results, error_score)
    421 # For callable scoring, the return type is only know after calling. If the
    422 # return type is a dictionary, the error scores can now be inserted with
    423 # the correct key.
    424 if callable(scoring):

File ~/git/manuscripts/cost-of-motion/.pixi/envs/default/lib/python3.14/site-packages/sklearn/model_selection/_validation.py:505, in _warn_or_raise_about_fit_failures(results, error_score)
    498 if num_failed_fits == num_fits:
    499     all_fits_failed_message = (
    500         f"\nAll the {num_fits} fits failed.\n"
    501         "It is very likely that your model is misconfigured.\n"
    502         "You can try to debug the error by setting error_score='raise'.\n\n"
    503         f"Below are more details about the failures:\n{fit_errors_summary}"
    504     )
--> 505     raise ValueError(all_fits_failed_message)
    507 else:
    508     some_fits_failed_message = (
    509         f"\n{num_failed_fits} fits failed out of a total of {num_fits}.\n"
    510         "The score on these train-test partitions for these parameters"
   (...)    514         f"Below are more details about the failures:\n{fit_errors_summary}"
    515     )

ValueError: 
All the 2 fits failed.
It is very likely that your model is misconfigured.
You can try to debug the error by setting error_score='raise'.

Below are more details about the failures:
--------------------------------------------------------------------------------
2 fits failed with the following error:
Traceback (most recent call last):
  File "/Users/psadil/git/manuscripts/cost-of-motion/.pixi/envs/default/lib/python3.14/site-packages/sklearn/model_selection/_validation.py", line 859, in _fit_and_score
    estimator.fit(X_train, y_train, **fit_params)
    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/psadil/git/manuscripts/cost-of-motion/.pixi/envs/default/lib/python3.14/site-packages/sklearn/base.py", line 1365, in wrapper
    return fit_method(estimator, *args, **kwargs)
  File "/Users/psadil/git/manuscripts/cost-of-motion/.pixi/envs/default/lib/python3.14/site-packages/sklearn/model_selection/_search.py", line 1051, in fit
    self._run_search(evaluate_candidates)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^
  File "/Users/psadil/git/manuscripts/cost-of-motion/.pixi/envs/default/lib/python3.14/site-packages/sklearn/model_selection/_search.py", line 1605, in _run_search
    evaluate_candidates(ParameterGrid(self.param_grid))
    ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/psadil/git/manuscripts/cost-of-motion/.pixi/envs/default/lib/python3.14/site-packages/sklearn/model_selection/_search.py", line 1009, in evaluate_candidates
    for (cand_idx, parameters), (split_idx, (train, test)) in product(
                                                              ~~~~~~~^
        enumerate(candidate_params),
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        enumerate(cv.split(X, y, **routed_params.splitter.split)),
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/Users/psadil/git/manuscripts/cost-of-motion/.pixi/envs/default/lib/python3.14/site-packages/sklearn/model_selection/_split.py", line 411, in split
    for train, test in super().split(X, y, groups):
                       ~~~~~~~~~~~~~^^^^^^^^^^^^^^
  File "/Users/psadil/git/manuscripts/cost-of-motion/.pixi/envs/default/lib/python3.14/site-packages/sklearn/model_selection/_split.py", line 142, in split
    for test_index in self._iter_test_masks(X, y, groups):
                      ~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^
  File "/Users/psadil/git/manuscripts/cost-of-motion/.pixi/envs/default/lib/python3.14/site-packages/sklearn/model_selection/_split.py", line 154, in _iter_test_masks
    for test_index in self._iter_test_indices(X, y, groups):
                      ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^
  File "/Users/psadil/git/manuscripts/cost-of-motion/.pixi/envs/default/lib/python3.14/site-packages/sklearn/model_selection/_split.py", line 612, in _iter_test_indices
    raise ValueError("The 'groups' parameter should not be None.")
ValueError: The 'groups' parameter should not be None.
```

### Versions

```shell
System:
    python: 3.14.0 | packaged by conda-forge | (main, Oct 12 2025, 19:48:32) [Clang 20.1.8 ]
executable: /Users/psadil/git/manuscripts/cost-of-motion/.pixi/envs/default/bin/python
   machine: macOS-26.0.1-arm64-arm-64bit-Mach-O

Python dependencies:
      sklearn: 1.7.2
          pip: None
   setuptools: 80.9.0
        numpy: 2.3.3
        scipy: 1.16.2
       Cython: None
       pandas: 2.3.3
   matplotlib: None
       joblib: 1.5.2
threadpoolctl: 3.6.0

Built with OpenMP: True

threadpoolctl info:
       user_api: blas
   internal_api: openblas
    num_threads: 10
         prefix: libopenblas
       filepath: /Users/psadil/git/manuscripts/cost-of-motion/.pixi/envs/default/lib/libopenblas.0.dylib
        version: 0.3.30
threading_layer: openmp
   architecture: VORTEX

       user_api: openmp
   internal_api: openmp
    num_threads: 10
         prefix: libomp
       filepath: /Users/psadil/git/manuscripts/cost-of-motion/.pixi/envs/default/lib/libomp.dylib
        version: None
```

COMMENT:
```python
model = hyper_cv.fit(X=X, y=y, groups=groups)
test_score = model_selection.cross_val_score(model, X, y, cv=group_kfold, groups=groups)
```

The first call to `fit` in the first of the above two lines has no impact on what happens in the second line: `cross_val_score` indeed calls `clone(model)` internally and therefore forgets everything about the previous call to `hyper_cv.fit`.

To implement nested group-aware cross-validation, you need to enable meta-data routing which is still an experimental feature and therefore not enabled by default:

https://scikit-learn.org/stable/metadata_routing.html

For your minimal reproducer, I think this would be to adapt it as follows:

```python
import numpy as np
from sklearn import model_selection, kernel_ridge, set_config

set_config(enable_metadata_routing=True)

X = np.array([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10], [11, 12]])
y = np.array([1, 2, 3, 4, 5, 6])
groups = np.array([0, 0, 2, 2, 3, 3])
inner_groupkfold = model_selection.GroupKFold(n_splits=2)
outer_groupkfold = model_selection.GroupKFold(n_splits=3)

krr = kernel_ridge.KernelRidge()
model = model_selection.GridSearchCV(
    krr,
    param_grid={"alpha": [0.1, 0.2]},
    cv=inner_groupkfold,
    scoring="r2",
    error_score="raise",
)
test_score = model_selection.cross_val_score(
    model, X, y, cv=outer_groupkfold, params=dict(groups=groups), error_score="raise"
)
print("Test scores:", test_score)
```

If that answers your problem, please subsequently close this issue as I don't think there is a bug to fix here.

Labels: Question