Skip to content

Commit 25b36ec

Browse files
8b fixes (#12)
* transformers bump + fa2 * patched data_loader * add patched data loaders to runners * load models w/ fa2 properly * add train / eval dataset handling * fmt * gitignore update for dataset cache * Add load_tokenizer helper and SFT_EXCLUDED_KEYS load_tokenizer loads only the tokenizer without model weights, for use during pre-tokenization on the driver. SFT_EXCLUDED_KEYS lists config keys that must be filtered when building TrainingArguments for plain Trainer (replacing SFTConfig). * Add pre-tokenization functions for SFT and DPO tokenize_sft / tokenize_and_pack_sft: distributed tokenization via Ray .map(), with optional BFD packing via TRL's pack_dataset. tokenize_dpo / tokenize_dpo_dataset: replicates DPOTrainer's tokenization pipeline (extract prompt, apply chat template, tokenize, append eos, truncate) as a Ray .map() step. * Rewrite SFT to use plain Trainer with pre-tokenized data Replace SFTTrainer/SFTConfig with Trainer/TrainingArguments since tokenization now happens centrally on the driver. Use TRL's DataCollatorForLanguageModeling for labels, padding, and padding-free mode. Get eval shard from Ray instead of materializing full eval on every worker. Fix import paths (configs -> training_configs) and extract run_name_template before filtering config keys. * Pre-tokenized DPO with PreTokenizedDPOTrainer subclass * Centralize tokenization on driver via Ray .map() * Add tokenization caching with parquet persistence * Simplify checkpoint callback to skip Ray checkpoint duplication * Fix checkpoint callback to call _rename_checkpoint on save * Use available memory instead of total for Ray object store * Make tokenization caching opt-in via cache_dataset flag * check if cuda available before committing time to ray workflow * consistent naming for cached dataset * _resolve_model_id already has this logic * Make tokenization caching opt-in via cache_dataset flag * Clear metrics after checkpoint report to avoid re-reporting stale values * Avoid full dataset materialization for non-packing SFT runs --------- Co-authored-by: EdoardoMosca <edoardo@liquid.ai>
1 parent b7c4dfc commit 25b36ec

14 files changed

Lines changed: 1970 additions & 1539 deletions

File tree

.gitignore

100644100755
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,6 @@ scripts/
2323

2424
# Quick testing files
2525
*.ipynb
26+
27+
# Tokenization cache
28+
.cache/

pyproject.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ dependencies = [
1212
"peft>=0.15.2",
1313
"deepspeed>=0.17.1",
1414
"torch>=2.7.1",
15-
"transformers @ git+https://github.com/huggingface/transformers.git@0c9a72e4576fe4c84077f066e585129c97bfd4e6",
15+
"transformers>=5.0.0",
16+
"flash-attn>=2.8.0",
17+
"numpy>=2.4.1",
1618
"trl>=0.18.2",
1719
"rich>=14.1.0",
1820
"pillow>=11.3.0",
@@ -31,8 +33,8 @@ leap-finetune = "leap_finetune:main"
3133
requires = ["hatchling"]
3234
build-backend = "hatchling.build"
3335

34-
[tool.hatch.metadata]
35-
allow-direct-references = true
36+
[tool.uv]
37+
no-build-isolation-package = ["flash-attn"]
3638

3739
[dependency-groups]
3840
dev = [

src/leap_finetune/data_loaders/dataset_loader.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class DatasetLoader:
2121
split: str = "train"
2222
test_size: float = 0.2
2323
subset: str | None = None
24+
cache_dataset: bool = False
2425
# Optional preprocessing function: takes Ray Dataset, returns Ray Dataset
2526
# Applied before validation - use for custom filtering, transforms, joins, etc.
2627
preprocess_fn: Callable | None = field(default=None, repr=False)

src/leap_finetune/data_loaders/ray_data_utils.py

Lines changed: 154 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,151 @@
1+
import hashlib
2+
import json
3+
import logging
4+
15
import ray.data
26
from datasets import Dataset
37
from rich.console import Console
48

9+
from leap_finetune.utils.constants import TOKENIZATION_CACHE_DIR
10+
511
from .dataset_loader import DatasetLoader
12+
from .tokenize_data import tokenize_and_pack_sft, tokenize_dpo_dataset
613
from .validate_loader import get_row_filter, normalize_columns
714

15+
logger = logging.getLogger(__name__)
16+
17+
18+
# === Tokenization Cache ===
19+
20+
21+
def _build_cache_key(
22+
loader: DatasetLoader,
23+
shuffle_seed: int,
24+
tokenizer_id: str,
25+
training_config: dict,
26+
) -> tuple[str, dict]:
27+
"""Build a deterministic cache key from all parameters affecting tokenized output."""
28+
dataset_type = loader.dataset_type
29+
30+
key = {
31+
"dataset_path": loader.dataset_path,
32+
"subset": loader.subset,
33+
"split": loader.split,
34+
"limit": loader.limit,
35+
"test_size": loader.test_size,
36+
"dataset_type": dataset_type,
37+
"tokenizer": tokenizer_id,
38+
"shuffle_seed": shuffle_seed,
39+
}
40+
41+
if dataset_type == "sft":
42+
key["max_length"] = training_config.get("max_length", 2048)
43+
key["packing"] = training_config.get("packing", False)
44+
elif dataset_type == "dpo":
45+
key["max_prompt_length"] = training_config.get("max_prompt_length")
46+
key["max_completion_length"] = training_config.get("max_completion_length")
47+
48+
canonical = json.dumps(key, sort_keys=True)
49+
fingerprint = hashlib.sha256(canonical.encode()).hexdigest()[:16]
50+
return fingerprint, key
51+
52+
53+
def _try_load_cache(
54+
fingerprint: str,
55+
) -> tuple[ray.data.Dataset, ray.data.Dataset] | None:
56+
"""Load cached train/eval parquet if the cache directory exists."""
57+
cache_dir = TOKENIZATION_CACHE_DIR / fingerprint
58+
train_dir = cache_dir / "train"
59+
eval_dir = cache_dir / "eval"
60+
61+
if not train_dir.exists() or not eval_dir.exists():
62+
return None
63+
64+
try:
65+
train_ds = ray.data.read_parquet(str(train_dir))
66+
eval_ds = ray.data.read_parquet(str(eval_dir))
67+
return train_ds, eval_ds
68+
except Exception:
69+
logger.warning("Failed to read tokenization cache, will re-tokenize")
70+
return None
71+
72+
73+
def _save_cache(
74+
fingerprint: str,
75+
train_ds: ray.data.Dataset,
76+
eval_ds: ray.data.Dataset,
77+
key_dict: dict,
78+
) -> None:
79+
"""Write train/eval datasets as parquet into the cache directory."""
80+
cache_dir = TOKENIZATION_CACHE_DIR / fingerprint
81+
cache_dir.mkdir(parents=True, exist_ok=True)
82+
83+
train_ds.write_parquet(str(cache_dir / "train"))
84+
eval_ds.write_parquet(str(cache_dir / "eval"))
85+
86+
(cache_dir / "fingerprint.json").write_text(json.dumps(key_dict, indent=2))
87+
888

989
def create_ray_datasets(
1090
loader: DatasetLoader,
1191
shuffle_seed: int = 42,
92+
tokenizer=None,
93+
training_config: dict | None = None,
1294
) -> tuple[ray.data.Dataset, ray.data.Dataset]:
1395
"""
1496
Create validated, shuffled, split Ray Datasets from a DatasetLoader.
1597
16-
Pipeline: quick_validate → load → [preprocess] → filter → normalize → shuffle → split
98+
Pipeline: quick_validate → load → [preprocess] → filter → normalize → shuffle → split → [tokenize/pack]
1799
18-
Uses Ray Data native operations (filter/map) - no pandas/arrow imports needed.
100+
When tokenizer is provided, tokenization and optional packing happen
101+
centrally before sharding, producing equal-length shards (±1 row).
102+
Tokenized results are cached as Parquet for subsequent runs.
19103
"""
20104
console = Console()
21105

106+
# === Check tokenization cache ===
107+
use_pretokenize = tokenizer is not None and training_config is not None
108+
can_cache = (
109+
use_pretokenize and loader.cache_dataset and loader.preprocess_fn is None
110+
)
111+
fingerprint = None
112+
key_dict = None
113+
114+
if can_cache:
115+
fingerprint, key_dict = _build_cache_key(
116+
loader, shuffle_seed, tokenizer.name_or_path, training_config
117+
)
118+
cached = _try_load_cache(fingerprint)
119+
if cached is not None:
120+
train_ds, eval_ds = cached
121+
train_count = train_ds.count()
122+
eval_count = eval_ds.count()
123+
console.print(
124+
f"[green]✓ Cache hit[/green] [dim]({fingerprint})[/dim]: "
125+
f"{train_count + eval_count:,} samples "
126+
f"(train: {train_count:,}, eval: {eval_count:,})"
127+
)
128+
return train_ds, eval_ds
129+
logger.info(
130+
"Tokenization cache miss (%s), will tokenize and cache", fingerprint
131+
)
132+
133+
# === Full pipeline: load → filter → normalize → shuffle → split → tokenize ===
134+
135+
loader.quick_validate()
22136
ds = loader.to_ray_dataset()
23137

24-
# Apply user preprocessing if provided (before validation)
25138
if loader.preprocess_fn is not None:
26139
console.print("[dim]Applying preprocessing...[/dim]")
27140
ds = loader.preprocess_fn(ds)
28141

29-
# Filter invalid rows using Ray's native filter (pure Python, Ray handles Arrow)
30142
row_filter = get_row_filter(loader.dataset_type)
31143
ds = ds.filter(row_filter)
32144

33-
# Normalize column names
34145
normalizer = normalize_columns(loader.dataset_type)
35146
ds = ds.map(normalizer)
36147

37-
# Shuffle before split
38148
ds = ds.random_shuffle(seed=shuffle_seed)
39-
40-
# Materialize to get count
41149
total_count = ds.count()
42150

43151
if total_count == 0:
@@ -46,7 +154,6 @@ def create_ray_datasets(
46154
f"the expected format for dataset_type='{loader.dataset_type}'"
47155
)
48156

49-
# Calculate split sizes
50157
eval_count = max(1, int(total_count * loader.test_size))
51158
train_count = total_count - eval_count
52159

@@ -56,14 +163,51 @@ def create_ray_datasets(
56163
f"with test_size={loader.test_size}"
57164
)
58165

59-
# Split into train/eval
60166
train_ds, eval_ds = ds.split_at_indices([train_count])
61167

62168
console.print(
63169
f"[green]✓ Dataset ready:[/green] {total_count:,} samples "
64170
f"(train: {train_count:,}, eval: {eval_count:,})"
65171
)
66172

173+
# === Pre-tokenize if tokenizer provided ===
174+
if use_pretokenize:
175+
dataset_type = loader.dataset_type
176+
177+
if dataset_type == "sft":
178+
max_length = training_config.get("max_length", 2048)
179+
packing = training_config.get("packing", False)
180+
181+
console.print(
182+
f"[dim]Tokenizing SFT (max_length={max_length}, packing={packing})...[/dim]"
183+
)
184+
train_ds = tokenize_and_pack_sft(train_ds, tokenizer, max_length, packing)
185+
eval_ds = tokenize_and_pack_sft(
186+
eval_ds, tokenizer, max_length, packing=False
187+
)
188+
189+
elif dataset_type == "dpo":
190+
max_prompt_length = training_config.get("max_prompt_length")
191+
max_completion_length = training_config.get("max_completion_length")
192+
193+
console.print("[dim]Tokenizing DPO...[/dim]")
194+
train_ds = tokenize_dpo_dataset(
195+
train_ds, tokenizer, max_prompt_length, max_completion_length
196+
)
197+
eval_ds = tokenize_dpo_dataset(
198+
eval_ds, tokenizer, max_prompt_length, max_completion_length
199+
)
200+
201+
# === Save to cache ===
202+
if can_cache and fingerprint is not None:
203+
try:
204+
_save_cache(fingerprint, train_ds, eval_ds, key_dict)
205+
console.print(f"[dim]Cached tokenized data ({fingerprint})[/dim]")
206+
except Exception:
207+
logger.warning(
208+
"Failed to write tokenization cache, continuing without cache"
209+
)
210+
67211
return train_ds, eval_ds
68212

69213

0 commit comments

Comments
 (0)