diff --git a/MIGRATION.md b/MIGRATION.md index 6877b331c72..ba2c8e2338a 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -15,6 +15,26 @@ This guide covers the breaking changes introduced in TRL v1 and how to update yo | --- | --- | --- | --- | --- | | `SFTConfig` | `packing` | `"bfd-requeue"` | `"bfd_split"` | Replace `packing="bfd-requeue"` with `packing="bfd_split"`. The old value will still be accepted for a few versions but will be removed in a future release. | +## Removed automatic `None` stripping from trainer preprocessing + +TRL trainers (SFT, DPO, Reward) no longer automatically strip `None` values from dataset examples during preprocessing. Previously, each trainer applied `remove_none_values` via `dataset.with_transform` to work around tabular backends (Arrow/Parquet) inserting `None` for missing keys in nested structures. + +This affects datasets that contain `None` values because they were: + +- Created before `datasets` v4.7.0, which introduced the Json dtype that preserves nested structures without inserting `None`. +- Created with `datasets` v4.7.0 or later, but saved without using the Json feature. + +**Action needed:** If your dataset falls into one of the above categories and contains `None` values in nested columns, apply the fix manually before training: + +```python +from trl.trainer.utils import remove_none_values + +dataset = dataset.with_transform(remove_none_values) +trainer = SFTTrainer(..., train_dataset=dataset) +``` + +Datasets created or re-saved with `datasets` v4.7.0+ using the Json dtype are unaffected. + ## Migrating from an earlier version Depending on which version you're migrating from, refer to the [release notes](https://github.com/huggingface/trl/releases) for v0.29 and earlier for version-specific changes. diff --git a/pyproject.toml b/pyproject.toml index ac9f4cc991f..f4831765478 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ classifiers = [ requires-python = ">=3.10" dependencies = [ "accelerate>=1.4.0", - "datasets>=3.0.0", + "datasets>=4.7.0", # Support Json type and on_mixed_types="use_json" "packaging>20.0", "transformers>=4.56.2", ] diff --git a/scripts/generate_harmony_dataset.py b/scripts/generate_harmony_dataset.py index 670c586a4bb..2257be8b7e6 100644 --- a/scripts/generate_harmony_dataset.py +++ b/scripts/generate_harmony_dataset.py @@ -91,7 +91,7 @@ def main(test_size, push_to_hub, repo_id): {"reasoning_effort": "high", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "low", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, ] - }) + }, on_mixed_types="use_json") language_modeling_dataset = language_modeling_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: language_modeling_dataset.push_to_hub(repo_id, config_name="language_modeling") diff --git a/scripts/generate_toolcall_dataset.py b/scripts/generate_toolcall_dataset.py index d3c9caa88d5..84b56b88ab9 100644 --- a/scripts/generate_toolcall_dataset.py +++ b/scripts/generate_toolcall_dataset.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json from dataclasses import dataclass, field from datasets import Dataset @@ -213,16 +212,16 @@ def get_wind_conditions(city: str, unit: str) -> tuple[int, str]: ] ], "tools": [ - json.dumps([start_timer, create_reminder]), - json.dumps([get_current_time]), - json.dumps([get_air_quality_index, get_weather_forecast, get_wind_conditions]), - json.dumps([play_music, control_light]), - json.dumps([get_weather_forecast, get_wind_conditions]), - json.dumps([control_light]), - json.dumps([start_timer, create_reminder]), - json.dumps([get_weather_forecast, get_wind_conditions]), + [start_timer, create_reminder], + [get_current_time], + [get_air_quality_index, get_weather_forecast, get_wind_conditions], + [play_music, control_light], + [get_weather_forecast, get_wind_conditions], + [control_light], + [start_timer, create_reminder], + [get_weather_forecast, get_wind_conditions], ] - }) + }, on_mixed_types="use_json") language_modeling_dataset = language_modeling_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: language_modeling_dataset.push_to_hub(repo_id, config_name="language_modeling") @@ -319,16 +318,16 @@ def get_wind_conditions(city: str, unit: str) -> tuple[int, str]: ], ], "tools": [ - json.dumps([start_timer]), - json.dumps([get_current_time]), - json.dumps([get_air_quality_index]), - json.dumps([play_music]), - json.dumps([get_weather_forecast]), - json.dumps([control_light]), - json.dumps([create_reminder]), - json.dumps([get_wind_conditions]), + [start_timer], + [get_current_time], + [get_air_quality_index], + [play_music], + [get_weather_forecast], + [control_light], + [create_reminder], + [get_wind_conditions], ], - }) + }, on_mixed_types="use_json") preference_dataset = preference_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: preference_dataset.push_to_hub(repo_id, config_name="preference") diff --git a/trl/trainer/dpo_trainer.py b/trl/trainer/dpo_trainer.py index d175d09b5ec..29f9102a0e3 100644 --- a/trl/trainer/dpo_trainer.py +++ b/trl/trainer/dpo_trainer.py @@ -59,7 +59,6 @@ get_config_model_id, hash_module, pad, - remove_none_values, selective_log_softmax, use_adapter, ) @@ -863,11 +862,6 @@ def _prepare_dataset( args: DPOConfig, dataset_name: str, ) -> Dataset | IterableDataset: - # Tabular backends like Arrow/Parquet insert `None` for mismatched keys in nested structures. Clean them from - # sampled data. - if isinstance(dataset, Dataset): # IterableDataset does not support `with_transform` - dataset = dataset.with_transform(remove_none_values) - # Build the kwargs for the `map` function map_kwargs = {} if isinstance(dataset, Dataset): # IterableDataset does not support num_proc diff --git a/trl/trainer/reward_trainer.py b/trl/trainer/reward_trainer.py index cd079499b51..5f19c1eb907 100644 --- a/trl/trainer/reward_trainer.py +++ b/trl/trainer/reward_trainer.py @@ -52,7 +52,7 @@ from ..models import get_act_offloading_ctx_manager from .base_trainer import _BaseTrainer from .reward_config import RewardConfig -from .utils import create_model_from_path, disable_dropout_in_model, get_config_model_id, pad, remove_none_values +from .utils import create_model_from_path, disable_dropout_in_model, get_config_model_id, pad if is_peft_available(): @@ -541,11 +541,6 @@ def _prepare_dataset( args: RewardConfig, dataset_name: str, ) -> Dataset | IterableDataset: - # Tabular backends like Arrow/Parquet insert `None` for mismatched keys in nested structures. Clean them from - # sampled data. - if isinstance(dataset, Dataset): # IterableDataset does not support `with_transform` - dataset = dataset.with_transform(remove_none_values) - # If the dataset is already preprocessed (tokenized), skip the processing steps. column_names = get_dataset_column_names(dataset) is_processed = "chosen_ids" in column_names and "rejected_ids" in column_names diff --git a/trl/trainer/sft_trainer.py b/trl/trainer/sft_trainer.py index 7207b98349a..59fb77b5707 100644 --- a/trl/trainer/sft_trainer.py +++ b/trl/trainer/sft_trainer.py @@ -61,7 +61,6 @@ flush_left, get_config_model_id, pad, - remove_none_values, selective_log_softmax, ) @@ -1054,11 +1053,6 @@ def _prepare_dataset( formatting_func: Callable[[dict], str] | None, dataset_name: str, ) -> Dataset | IterableDataset: - # Tabular backends like Arrow/Parquet insert `None` for mismatched keys in nested structures. Clean them from - # sampled data. - if isinstance(dataset, Dataset): # IterableDataset does not support `with_transform` - dataset = dataset.with_transform(remove_none_values) - # If the dataset is already preprocessed (tokenized), skip the processing steps. column_names = get_dataset_column_names(dataset) is_processed = "input_ids" in column_names diff --git a/trl/trainer/utils.py b/trl/trainer/utils.py index 1db504acebe..7e22e241c1a 100644 --- a/trl/trainer/utils.py +++ b/trl/trainer/utils.py @@ -972,6 +972,12 @@ def unsplit_pixel_values_by_grid(batch: dict[str, torch.Tensor | list[torch.Tens TListOrMapping = TypeVar("TListOrMapping", list, Mapping) +# This function is intentionally not used internally. It is provided as a utility for users whose datasets contain +# `None` values inserted by tabular backends (e.g., Arrow/Parquet) for missing keys in nested structures. This +# situation arises when loading datasets created before `datasets` v4.7.0 (which introduced the Json dtype), or when +# datasets created after that version were saved without using the Json feature. In both cases, users can apply this +# function via `dataset = dataset.with_transform(remove_none_values)` before training to strip the spurious `None` +# values. See the migration guide for more details. def remove_none_values(example: TListOrMapping) -> TListOrMapping: """ Recursively removes entries with `None` values from a nested structure (list or dictionary). @@ -980,7 +986,10 @@ def remove_none_values(example: TListOrMapping) -> TListOrMapping: example (`list` or `Mapping`): Input nested structure (list or dictionary) from which to remove `None`. - Example: + Examples: + ```python + >>> dataset = dataset.with_transform(remove_none_values) + ``` ```python >>> [ ... {