jsantillana commited on
Commit
b03dc58
·
verified ·
1 Parent(s): 56f6624

Upload training_v2/data/sft_dataset.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. training_v2/data/sft_dataset.py +124 -0
training_v2/data/sft_dataset.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """SFT dataset with proper assistant-only loss masking and safe packing.
2
+
3
+ Each example is a chat-formatted string with `<|system|> <|user|> <|assistant|> <|end|>`
4
+ turn delimiters. We tokenize on the fly (corpus is small, ~25M tokens) and build a
5
+ mask=1 only on tokens that are part of an assistant response (everything between
6
+ `<|assistant|>` and the next `<|end|>`).
7
+
8
+ For pre-training-style packing without cross-example contamination we group multiple
9
+ short examples into a fixed-length window using `cu_seqlens`-style document boundaries
10
+ implemented via per-document attention reset. Here we keep it simple: pad/truncate
11
+ each example to `block_size`. Throughput is still high (>40k tok/s on L4) for this
12
+ volume.
13
+ """
14
+
15
+ import json
16
+ import random
17
+ from pathlib import Path
18
+
19
+ import numpy as np
20
+ import torch
21
+ from torch.utils.data import Dataset
22
+
23
+
24
+ def _messages_to_text(messages):
25
+ """Convierte lista de {role, content} al formato de chat con special tokens."""
26
+ parts = []
27
+ for m in messages:
28
+ role = m.get("role", "")
29
+ content = m.get("content", "").strip()
30
+ if role == "system":
31
+ parts.append(f"<|system|>{content}<|end|>")
32
+ elif role == "user":
33
+ parts.append(f"<|user|>{content}<|end|>")
34
+ elif role == "assistant":
35
+ parts.append(f"<|assistant|>{content}<|end|>")
36
+ return "".join(parts)
37
+
38
+
39
+ def _read_jsonl(path):
40
+ out = []
41
+ with open(path, "r", encoding="utf-8", errors="replace") as f:
42
+ for line in f:
43
+ line = line.strip()
44
+ if not line:
45
+ continue
46
+ try:
47
+ obj = json.loads(line)
48
+ except json.JSONDecodeError:
49
+ continue
50
+ t = obj.get("text") or ""
51
+ if not t and "messages" in obj:
52
+ msgs = obj["messages"]
53
+ if isinstance(msgs, list) and len(msgs) >= 2:
54
+ t = _messages_to_text(msgs)
55
+ if t:
56
+ out.append({"text": t, "source": obj.get("source", Path(path).stem)})
57
+ return out
58
+
59
+
60
+ def build_assistant_mask(token_ids, assistant_id, end_id):
61
+ """mask[i] = 1 iff token_ids[i] is inside an `<|assistant|> ... <|end|>` span.
62
+
63
+ We mark from the token AFTER `<|assistant|>` up to and including `<|end|>` so the
64
+ model learns to emit the closing delimiter.
65
+ """
66
+ mask = np.zeros(len(token_ids), dtype=np.int64)
67
+ inside = False
68
+ for i, t in enumerate(token_ids):
69
+ if t == assistant_id and not inside:
70
+ inside = True
71
+ continue # don't include the assistant tag itself
72
+ if inside:
73
+ mask[i] = 1
74
+ if t == end_id:
75
+ inside = False
76
+ return mask
77
+
78
+
79
+ class SFTDataset(Dataset):
80
+ def __init__(self, jsonl_paths, sp, block_size, assistant_token="<|assistant|>",
81
+ end_token="<|end|>", pad_id=0, seed=42, mix_weights=None):
82
+ self.sp = sp
83
+ self.block_size = block_size
84
+ self.pad_id = pad_id
85
+ self.assistant_id = sp.piece_to_id(assistant_token)
86
+ self.end_id = sp.piece_to_id(end_token)
87
+ if self.assistant_id < 0 or self.end_id < 0:
88
+ raise ValueError(f"missing special tokens in tokenizer: "
89
+ f"{assistant_token}={self.assistant_id} {end_token}={self.end_id}")
90
+
91
+ self.examples = []
92
+ rng = random.Random(seed)
93
+ for p in jsonl_paths:
94
+ recs = _read_jsonl(p)
95
+ w = (mix_weights or {}).get(Path(p).name, 1.0)
96
+ if w != 1.0:
97
+ k = int(len(recs) * w)
98
+ recs = rng.sample(recs, min(k, len(recs)))
99
+ self.examples.extend(recs)
100
+ print(f" [sft] {p}: {len(recs):,} ex (w={w})")
101
+ rng.shuffle(self.examples)
102
+ print(f"[sft] total: {len(self.examples):,} examples")
103
+
104
+ def __len__(self):
105
+ return len(self.examples)
106
+
107
+ def __getitem__(self, idx):
108
+ text = self.examples[idx]["text"]
109
+ ids = self.sp.encode(text, out_type=int)
110
+ ids = ids[: self.block_size + 1]
111
+ mask = build_assistant_mask(ids, self.assistant_id, self.end_id)
112
+
113
+ if len(ids) < self.block_size + 1:
114
+ need = self.block_size + 1 - len(ids)
115
+ ids = ids + [self.pad_id] * need
116
+ mask = np.concatenate([mask, np.zeros(need, dtype=np.int64)])
117
+
118
+ ids = np.asarray(ids, dtype=np.int64)
119
+ x = torch.from_numpy(ids[:-1])
120
+ y = torch.from_numpy(ids[1:].copy())
121
+ m = torch.from_numpy(mask[1:].copy()) # mask aligned with targets
122
+ # zero out padded targets
123
+ y[m == 0] = -100
124
+ return x, y, m