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 +19 -0
- trloom/__main__.py +8 -0
- trloom/cli.py +148 -0
- trloom/config/__init__.py +22 -0
- trloom/config/loader.py +36 -0
- trloom/config/schema.py +185 -0
- trloom/data/__init__.py +7 -0
- trloom/data/dataset.py +210 -0
- trloom/job.py +109 -0
- trloom/logging_utils/__init__.py +106 -0
- trloom/modal_support/__init__.py +7 -0
- trloom/modal_support/runner.py +293 -0
- trloom/rewards.py +81 -0
- trloom/trainers/__init__.py +13 -0
- trloom/trainers/builder.py +149 -0
- trloom/trainers/registry.py +221 -0
- trloom-0.1.0.dist-info/METADATA +305 -0
- trloom-0.1.0.dist-info/RECORD +21 -0
- trloom-0.1.0.dist-info/WHEEL +4 -0
- trloom-0.1.0.dist-info/entry_points.txt +2 -0
- trloom-0.1.0.dist-info/licenses/LICENSE +176 -0
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
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
|
+
]
|
trloom/config/loader.py
ADDED
|
@@ -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)
|
trloom/config/schema.py
ADDED
|
@@ -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()
|
trloom/data/__init__.py
ADDED
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
|