trloom 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
trloom/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """TRLoom: YAML-driven end-to-end fine-tuning on top of Hugging Face TRL."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from trloom.config import FineTuneConfig, load_config
6
+ from trloom.job import FineTuneJob, available_methods, run_from_yaml
7
+ from trloom.trainers import list_trainers
8
+
9
+ __all__ = [
10
+ "FineTuneConfig",
11
+ "FineTuneJob",
12
+ "__version__",
13
+ "available_methods",
14
+ "list_trainers",
15
+ "load_config",
16
+ "run_from_yaml",
17
+ ]
18
+
19
+ __version__ = "0.1.0"
trloom/__main__.py ADDED
@@ -0,0 +1,8 @@
1
+ """python -m trloom"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from trloom.cli import main
6
+
7
+ if __name__ == "__main__": # pragma: no cover
8
+ raise SystemExit(main())
trloom/cli.py ADDED
@@ -0,0 +1,148 @@
1
+ """Command-line interface for TRLoom."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import logging
8
+ import sys
9
+ from pathlib import Path
10
+
11
+
12
+ def _configure_logging(verbose: bool) -> None:
13
+ level = logging.DEBUG if verbose else logging.INFO
14
+ logging.basicConfig(
15
+ level=level,
16
+ format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
17
+ )
18
+
19
+
20
+ def build_parser() -> argparse.ArgumentParser:
21
+ parser = argparse.ArgumentParser(
22
+ prog="trloom",
23
+ description="YAML-driven fine-tuning on top of Hugging Face TRL.",
24
+ )
25
+ parser.add_argument(
26
+ "--version",
27
+ action="store_true",
28
+ help="Print version and exit.",
29
+ )
30
+ parser.add_argument(
31
+ "-v",
32
+ "--verbose",
33
+ action="store_true",
34
+ help="Enable debug logging.",
35
+ )
36
+
37
+ subparsers = parser.add_subparsers(dest="command")
38
+
39
+ run_parser = subparsers.add_parser("run", help="Run a fine-tuning job from a YAML config.")
40
+ run_parser.add_argument("config", type=str, help="Path to the YAML configuration file.")
41
+ run_parser.add_argument(
42
+ "--modal",
43
+ action="store_true",
44
+ help="Force execution on Modal (overrides modal.enabled in YAML).",
45
+ )
46
+ run_parser.add_argument(
47
+ "--local",
48
+ action="store_true",
49
+ help="Force local execution (overrides modal.enabled in YAML).",
50
+ )
51
+
52
+ subparsers.add_parser("methods", help="List TRL training methods available in this environment.")
53
+
54
+ validate_parser = subparsers.add_parser("validate", help="Validate a YAML config without training.")
55
+ validate_parser.add_argument("config", type=str, help="Path to the YAML configuration file.")
56
+
57
+ modal_parser = subparsers.add_parser(
58
+ "modal-script",
59
+ help="Write a standalone Modal entrypoint script for a YAML config.",
60
+ )
61
+ modal_parser.add_argument("config", type=str, help="Path to the YAML configuration file.")
62
+ modal_parser.add_argument(
63
+ "-o",
64
+ "--output",
65
+ type=str,
66
+ default=None,
67
+ help="Destination path for the generated script.",
68
+ )
69
+
70
+ return parser
71
+
72
+
73
+ def main(argv: list[str] | None = None) -> int:
74
+ parser = build_parser()
75
+ args = parser.parse_args(argv)
76
+ _configure_logging(getattr(args, "verbose", False))
77
+
78
+ if args.version:
79
+ from trloom import __version__
80
+
81
+ print(__version__)
82
+ return 0
83
+
84
+ if args.command is None:
85
+ parser.print_help()
86
+ return 0
87
+
88
+ if args.command == "methods":
89
+ from trloom import available_methods
90
+
91
+ methods = available_methods(include_experimental=True)
92
+ for method in methods:
93
+ print(method)
94
+ return 0
95
+
96
+ if args.command == "validate":
97
+ from trloom import load_config
98
+ from trloom.trainers import get_trainer_spec
99
+
100
+ config = load_config(args.config)
101
+ spec = get_trainer_spec(config.method)
102
+ payload = {
103
+ "ok": True,
104
+ "method": config.method,
105
+ "trainer": spec.trainer_name,
106
+ "config_class": spec.config_name,
107
+ "experimental": spec.experimental,
108
+ "output_dir": str(config.resolved_output_dir()),
109
+ "wandb_enabled": config.wandb.enabled,
110
+ "modal_enabled": config.modal.enabled,
111
+ }
112
+ print(json.dumps(payload, indent=2))
113
+ return 0
114
+
115
+ if args.command == "modal-script":
116
+ from trloom.modal_support.runner import write_modal_entrypoint
117
+
118
+ dest = write_modal_entrypoint(args.config, destination=args.output)
119
+ print(dest)
120
+ return 0
121
+
122
+ if args.command == "run":
123
+ from trloom import run_from_yaml
124
+
125
+ if args.modal and args.local:
126
+ parser.error("Use only one of --modal / --local.")
127
+ use_modal: bool | None
128
+ if args.modal:
129
+ use_modal = True
130
+ elif args.local:
131
+ use_modal = False
132
+ else:
133
+ use_modal = None
134
+
135
+ config_path = Path(args.config)
136
+ if not config_path.is_file():
137
+ print(f"Config not found: {config_path}", file=sys.stderr)
138
+ return 1
139
+
140
+ run_from_yaml(config_path, use_modal=use_modal)
141
+ return 0
142
+
143
+ parser.print_help()
144
+ return 1
145
+
146
+
147
+ if __name__ == "__main__": # pragma: no cover
148
+ raise SystemExit(main())
@@ -0,0 +1,22 @@
1
+ """Configuration schema and YAML loading utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from trloom.config.loader import load_config, load_config_dict
6
+ from trloom.config.schema import (
7
+ DatasetConfig,
8
+ FineTuneConfig,
9
+ ModalConfig,
10
+ ModelConfig,
11
+ WandbConfig,
12
+ )
13
+
14
+ __all__ = [
15
+ "DatasetConfig",
16
+ "FineTuneConfig",
17
+ "ModalConfig",
18
+ "ModelConfig",
19
+ "WandbConfig",
20
+ "load_config",
21
+ "load_config_dict",
22
+ ]
@@ -0,0 +1,36 @@
1
+ """Load and validate TRLoom YAML configuration files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import yaml
9
+
10
+ from trloom.config.schema import FineTuneConfig
11
+
12
+
13
+ def _read_yaml(path: Path) -> dict[str, Any]:
14
+ with path.open("r", encoding="utf-8") as handle:
15
+ data = yaml.safe_load(handle)
16
+ if data is None:
17
+ raise ValueError(f"Config file is empty: {path}")
18
+ if not isinstance(data, dict):
19
+ raise TypeError(f"Config root must be a mapping, got {type(data).__name__}: {path}")
20
+ return data
21
+
22
+
23
+ def load_config_dict(source: str | Path | dict[str, Any]) -> dict[str, Any]:
24
+ """Return a raw configuration dictionary from a path or mapping."""
25
+ if isinstance(source, dict):
26
+ return dict(source)
27
+ path = Path(source).expanduser().resolve()
28
+ if not path.is_file():
29
+ raise FileNotFoundError(f"Config file not found: {path}")
30
+ return _read_yaml(path)
31
+
32
+
33
+ def load_config(source: str | Path | dict[str, Any]) -> FineTuneConfig:
34
+ """Load and validate a :class:`FineTuneConfig` from YAML or a dict."""
35
+ data = load_config_dict(source)
36
+ return FineTuneConfig.model_validate(data)
@@ -0,0 +1,185 @@
1
+ """Pydantic models for TRLoom YAML configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any, Literal
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
9
+
10
+
11
+ class ModelConfig(BaseModel):
12
+ """Model / PEFT / quantization settings (mirrors TRL ``ModelConfig`` fields)."""
13
+
14
+ model_config = ConfigDict(extra="allow")
15
+
16
+ model_name_or_path: str
17
+ model_revision: str = "main"
18
+ dtype: Literal["auto", "bfloat16", "float16", "float32"] | None = "float32"
19
+ attn_implementation: str | None = None
20
+ trust_remote_code: bool = False
21
+
22
+ use_peft: bool = False
23
+ lora_r: int = 16
24
+ lora_alpha: int = 32
25
+ lora_dropout: float = 0.05
26
+ lora_target_modules: list[str] | str | None = None
27
+ lora_target_parameters: list[str] | None = None
28
+ lora_modules_to_save: list[str] | None = None
29
+ lora_task_type: str = "CAUSAL_LM"
30
+ use_rslora: bool = False
31
+ use_dora: bool = False
32
+
33
+ load_in_8bit: bool = False
34
+ load_in_4bit: bool = False
35
+ bnb_4bit_quant_type: Literal["fp4", "nf4"] = "nf4"
36
+ use_bnb_nested_quant: bool = False
37
+ bnb_4bit_quant_storage: str | None = None
38
+
39
+ @model_validator(mode="after")
40
+ def _validate_quantization(self) -> ModelConfig:
41
+ if self.load_in_8bit and self.load_in_4bit:
42
+ raise ValueError("Cannot enable both load_in_8bit and load_in_4bit.")
43
+ return self
44
+
45
+
46
+ class DatasetSourceConfig(BaseModel):
47
+ """A single dataset entry (Hub repo id or local path)."""
48
+
49
+ model_config = ConfigDict(extra="allow")
50
+
51
+ path: str
52
+ name: str | None = None
53
+ split: str | None = None
54
+ data_files: str | list[str] | dict[str, Any] | None = None
55
+ data_dir: str | None = None
56
+ streaming: bool = False
57
+ columns: dict[str, str] | None = None
58
+ weight: float | None = None
59
+
60
+
61
+ class DatasetConfig(BaseModel):
62
+ """Dataset loading configuration.
63
+
64
+ Prefer ``path`` for a single Hub/local dataset, or ``datasets`` for a mixture.
65
+ """
66
+
67
+ model_config = ConfigDict(extra="allow")
68
+
69
+ path: str | None = None
70
+ name: str | None = None
71
+ # Optional datasets split selector, e.g. "train" or "train[:64]"
72
+ split: str | None = None
73
+ data_files: str | list[str] | dict[str, Any] | None = None
74
+ data_dir: str | None = None
75
+ streaming: bool = False
76
+
77
+ datasets: list[DatasetSourceConfig] | None = None
78
+
79
+ train_split: str = "train"
80
+ eval_split: str | None = "test"
81
+ text_column: str | None = None
82
+ columns: dict[str, str] | None = None
83
+
84
+ # Optional Hugging Face datasets kwargs passthrough
85
+ kwargs: dict[str, Any] = Field(default_factory=dict)
86
+
87
+ @model_validator(mode="after")
88
+ def _require_source(self) -> DatasetConfig:
89
+ if not self.path and not self.datasets:
90
+ raise ValueError("Dataset config requires either 'path' or 'datasets'.")
91
+ return self
92
+
93
+
94
+ class WandbConfig(BaseModel):
95
+ """Weights & Biases logging settings."""
96
+
97
+ model_config = ConfigDict(extra="allow")
98
+
99
+ enabled: bool = False
100
+ project: str | None = None
101
+ entity: str | None = None
102
+ run_name: str | None = None
103
+ group: str | None = None
104
+ tags: list[str] = Field(default_factory=list)
105
+ notes: str | None = None
106
+ mode: Literal["online", "offline", "disabled"] | None = None
107
+ dir: str | None = None
108
+ job_type: str | None = "train"
109
+ # Extra keys forwarded to wandb.init
110
+ init_kwargs: dict[str, Any] = Field(default_factory=dict)
111
+
112
+
113
+ class ModalConfig(BaseModel):
114
+ """Modal Labs remote execution settings."""
115
+
116
+ model_config = ConfigDict(extra="allow")
117
+
118
+ enabled: bool = False
119
+ app_name: str = "trloom"
120
+ gpu: str = "T4"
121
+ timeout: int = 60 * 60 * 4
122
+ cpu: float | None = None
123
+ memory: int | None = None
124
+ volume_name: str = "trloom-outputs"
125
+ volume_mount: str = "/outputs"
126
+ # Names of Modal Secrets to attach (create with `modal secret create ...`).
127
+ # Leave empty for public Hub models/datasets that need no tokens.
128
+ secrets: list[str] = Field(default_factory=list)
129
+ pip_packages: list[str] = Field(default_factory=list)
130
+ python_version: str = "3.11"
131
+ region: str | None = None
132
+ # How to get trloom into the Modal image:
133
+ # - local: mount the local installed package (best for development)
134
+ # - git: pip install from git_url (best for reproducibility)
135
+ # - pypi: pip install trloom (once published)
136
+ install_source: Literal["local", "git", "pypi"] = "local"
137
+ git_url: str = "git+https://github.com/saqlain2204/trloom.git"
138
+ # If set, copy training output from the Modal volume back to this local path
139
+ download_dir: str | None = None
140
+
141
+
142
+ class FineTuneConfig(BaseModel):
143
+ """Root configuration for a TRLoom fine-tuning job."""
144
+
145
+ model_config = ConfigDict(extra="allow")
146
+
147
+ method: str = Field(
148
+ ...,
149
+ description="TRL training method, e.g. sft, dpo, grpo, kto, reward, rloo.",
150
+ )
151
+ model: ModelConfig
152
+ dataset: DatasetConfig
153
+ training: dict[str, Any] = Field(
154
+ default_factory=dict,
155
+ description="Keyword arguments forwarded to the TRL *Config class "
156
+ "(SFTConfig, DPOConfig, …).",
157
+ )
158
+ wandb: WandbConfig = Field(default_factory=WandbConfig)
159
+ modal: ModalConfig = Field(default_factory=ModalConfig)
160
+
161
+ # Optional extras
162
+ reward_funcs: list[str] | str | None = None
163
+ trainer_kwargs: dict[str, Any] = Field(
164
+ default_factory=dict,
165
+ description="Extra kwargs passed directly to the TRL Trainer constructor.",
166
+ )
167
+ push_to_hub: bool = False
168
+ hub_model_id: str | None = None
169
+ seed: int | None = 42
170
+
171
+ @field_validator("method")
172
+ @classmethod
173
+ def _normalize_method(cls, value: str) -> str:
174
+ return value.strip().lower().replace("-", "_").replace(" ", "_")
175
+
176
+ @field_validator("reward_funcs", mode="before")
177
+ @classmethod
178
+ def _coerce_reward_funcs(cls, value: Any) -> Any:
179
+ if isinstance(value, str):
180
+ return [value]
181
+ return value
182
+
183
+ def resolved_output_dir(self) -> Path:
184
+ output_dir = self.training.get("output_dir", "./outputs")
185
+ return Path(output_dir).expanduser().resolve()
@@ -0,0 +1,7 @@
1
+ """Dataset loading helpers for Hub and local sources."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from trloom.data.dataset import load_train_eval_datasets
6
+
7
+ __all__ = ["load_train_eval_datasets"]
trloom/data/dataset.py ADDED
@@ -0,0 +1,210 @@
1
+ """Load training datasets from the Hugging Face Hub or local files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from datasets import Dataset, DatasetDict, IterableDataset, IterableDatasetDict, concatenate_datasets, load_dataset
10
+
11
+ from trloom.config.schema import DatasetConfig, DatasetSourceConfig
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ DatasetLike = Dataset | DatasetDict | IterableDataset | IterableDatasetDict
16
+
17
+
18
+ def _is_local_path(path: str) -> bool:
19
+ candidate = Path(path).expanduser()
20
+ return candidate.exists() or path.startswith((".", "/", "~", "\\")) or ":\\" in path
21
+
22
+
23
+ def _infer_builder(path: str) -> str | None:
24
+ """Infer a datasets builder name from a local file extension."""
25
+ suffix = Path(path).suffix.lower()
26
+ mapping = {
27
+ ".json": "json",
28
+ ".jsonl": "json",
29
+ ".csv": "csv",
30
+ ".tsv": "csv",
31
+ ".parquet": "parquet",
32
+ ".txt": "text",
33
+ ".arrow": "arrow",
34
+ }
35
+ return mapping.get(suffix)
36
+
37
+
38
+ def _rename_columns(dataset: Any, columns: dict[str, str] | None) -> Any:
39
+ if not columns:
40
+ return dataset
41
+ rename_map = {src: dst for src, dst in columns.items() if src in getattr(dataset, "column_names", [])}
42
+ if not rename_map:
43
+ return dataset
44
+ return dataset.rename_columns(rename_map)
45
+
46
+
47
+ def _load_single_source(
48
+ source: DatasetSourceConfig | DatasetConfig,
49
+ *,
50
+ default_split: str | None = None,
51
+ ) -> DatasetLike:
52
+ path = source.path
53
+ if path is None:
54
+ raise ValueError("Dataset source is missing 'path'.")
55
+
56
+ kwargs: dict[str, Any] = {}
57
+ if isinstance(source, DatasetConfig):
58
+ kwargs.update(source.kwargs)
59
+ # Allow extras from DatasetSourceConfig / DatasetConfig
60
+ extras = source.model_extra or {}
61
+ for key, value in extras.items():
62
+ if key not in {"columns", "weight"}:
63
+ kwargs.setdefault(key, value)
64
+
65
+ name = source.name
66
+ data_files = source.data_files
67
+ data_dir = source.data_dir
68
+ streaming = bool(source.streaming)
69
+ split = source.split if getattr(source, "split", None) else default_split
70
+
71
+ if data_files is not None:
72
+ kwargs["data_files"] = data_files
73
+ if data_dir is not None:
74
+ kwargs["data_dir"] = data_dir
75
+
76
+ if _is_local_path(path):
77
+ local = Path(path).expanduser().resolve()
78
+ if local.is_dir():
79
+ # load_from_disk for saved DatasetDict / Dataset
80
+ try:
81
+ from datasets import load_from_disk
82
+
83
+ logger.info("Loading local dataset from disk: %s", local)
84
+ dataset = load_from_disk(str(local))
85
+ except Exception:
86
+ logger.info("Falling back to load_dataset for directory: %s", local)
87
+ dataset = load_dataset(str(local), name=name, split=split, streaming=streaming, **kwargs)
88
+ else:
89
+ builder = _infer_builder(str(local))
90
+ if builder is None and data_files is None:
91
+ raise ValueError(
92
+ f"Unsupported local dataset file type for '{local}'. "
93
+ "Use json/jsonl/csv/tsv/parquet/txt/arrow or provide data_files."
94
+ )
95
+ logger.info("Loading local dataset file via builder=%s path=%s", builder, local)
96
+ load_path = builder or str(local)
97
+ file_kwargs = dict(kwargs)
98
+ if builder and "data_files" not in file_kwargs:
99
+ file_kwargs["data_files"] = str(local)
100
+ dataset = load_dataset(load_path, name=name, split=split, streaming=streaming, **file_kwargs)
101
+ else:
102
+ logger.info("Loading Hub dataset: %s (name=%s, split=%s)", path, name, split)
103
+ dataset = load_dataset(path, name=name, split=split, streaming=streaming, **kwargs)
104
+
105
+ columns = getattr(source, "columns", None)
106
+ if columns and hasattr(dataset, "column_names"):
107
+ dataset = _rename_columns(dataset, columns)
108
+ elif columns and isinstance(dataset, (DatasetDict, IterableDatasetDict)):
109
+ dataset = type(dataset)({k: _rename_columns(v, columns) for k, v in dataset.items()})
110
+
111
+ return dataset
112
+
113
+
114
+ def _as_dataset_dict(dataset: DatasetLike, train_split: str) -> DatasetDict | IterableDatasetDict:
115
+ if isinstance(dataset, (DatasetDict, IterableDatasetDict)):
116
+ return dataset
117
+ # Single split dataset — wrap under train_split key
118
+ if isinstance(dataset, IterableDataset):
119
+ return IterableDatasetDict({train_split: dataset})
120
+ return DatasetDict({train_split: dataset})
121
+
122
+
123
+ def _mix_datasets(
124
+ sources: list[DatasetSourceConfig],
125
+ *,
126
+ train_split: str,
127
+ ) -> DatasetDict | IterableDatasetDict:
128
+ loaded: list[Dataset | IterableDataset] = []
129
+ weights: list[float] = []
130
+ streaming = False
131
+
132
+ for source in sources:
133
+ ds = _load_single_source(source, default_split=source.split or train_split)
134
+ if isinstance(ds, (DatasetDict, IterableDatasetDict)):
135
+ if train_split not in ds:
136
+ raise KeyError(
137
+ f"Mixture source '{source.path}' has no split '{train_split}'. "
138
+ f"Available: {list(ds.keys())}"
139
+ )
140
+ part = ds[train_split]
141
+ else:
142
+ part = ds
143
+ if isinstance(part, IterableDataset):
144
+ streaming = True
145
+ loaded.append(part)
146
+ weights.append(float(source.weight) if source.weight is not None else 1.0)
147
+
148
+ if streaming:
149
+ # Interleave for streaming mixtures; fall back to concatenate when weights are equal.
150
+ from datasets import interleave_datasets
151
+
152
+ mixed = interleave_datasets(loaded, probabilities=_normalize(weights), seed=42)
153
+ return IterableDatasetDict({train_split: mixed})
154
+
155
+ if len(set(weights)) == 1:
156
+ mixed = concatenate_datasets(loaded) # type: ignore[arg-type]
157
+ else:
158
+ from datasets import interleave_datasets
159
+
160
+ mixed = interleave_datasets(loaded, probabilities=_normalize(weights), seed=42)
161
+ return DatasetDict({train_split: mixed})
162
+
163
+
164
+ def _normalize(weights: list[float]) -> list[float]:
165
+ total = sum(weights)
166
+ if total <= 0:
167
+ raise ValueError("Dataset mixture weights must sum to a positive value.")
168
+ return [w / total for w in weights]
169
+
170
+
171
+ def load_train_eval_datasets(
172
+ config: DatasetConfig,
173
+ ) -> tuple[Dataset | IterableDataset, Dataset | IterableDataset | None]:
174
+ """Load train (and optional eval) datasets from a :class:`DatasetConfig`."""
175
+ if config.datasets:
176
+ dataset_dict = _mix_datasets(config.datasets, train_split=config.train_split)
177
+ else:
178
+ assert config.path is not None
179
+ raw = _load_single_source(config)
180
+ dataset_dict = _as_dataset_dict(raw, config.train_split)
181
+ if config.columns:
182
+ if isinstance(dataset_dict, (DatasetDict, IterableDatasetDict)):
183
+ dataset_dict = type(dataset_dict)(
184
+ {k: _rename_columns(v, config.columns) for k, v in dataset_dict.items()}
185
+ )
186
+
187
+ if config.train_split not in dataset_dict:
188
+ available = list(dataset_dict.keys())
189
+ raise KeyError(
190
+ f"Train split '{config.train_split}' not found in dataset. Available splits: {available}"
191
+ )
192
+
193
+ train_dataset = dataset_dict[config.train_split]
194
+ eval_dataset: Dataset | IterableDataset | None = None
195
+ if config.eval_split and config.eval_split in dataset_dict:
196
+ eval_dataset = dataset_dict[config.eval_split]
197
+
198
+ if config.text_column and hasattr(train_dataset, "column_names"):
199
+ if config.text_column not in train_dataset.column_names:
200
+ raise KeyError(
201
+ f"text_column '{config.text_column}' not found. "
202
+ f"Columns: {train_dataset.column_names}"
203
+ )
204
+
205
+ logger.info(
206
+ "Loaded train dataset (%s)%s",
207
+ type(train_dataset).__name__,
208
+ f" and eval split '{config.eval_split}'" if eval_dataset is not None else "",
209
+ )
210
+ return train_dataset, eval_dataset