-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbpe.py
More file actions
186 lines (153 loc) · 6.66 KB
/
Copy pathbpe.py
File metadata and controls
186 lines (153 loc) · 6.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
import os
import re
from collections import defaultdict
from typing import BinaryIO
from multiprocessing import Pool
class BPETokenizer:
# GPT-2 regex pre-tokenizer
PAT = r"""'(?:[sdmt]|ll|ve|re)| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+"""
TOKENIZER = re.compile(PAT, re.IGNORECASE)
SPECIAL_TOKEN = "<|endofdoc|>"
def __init__(self):
self.vocab: dict[tuple[int, ...], int] = {}
self.inv_vocab: dict[int, tuple[int, ...]] = {}
self.merges: list[tuple] = []
self.next_id: int = 0
# ---------- Chunking for parallel pre-tokenization ----------
def find_chunk_boundaries(
self, file: BinaryIO, desired_num_chunks: int, split_special_token: bytes
) -> list[int]:
"""Find safe chunk boundaries in a file for parallel pre-tokenization."""
assert isinstance(split_special_token, bytes), "split_special_token must be bytes"
file.seek(0, os.SEEK_END)
file_size = file.tell()
file.seek(0)
chunk_size = file_size // desired_num_chunks
chunk_boundaries = [i * chunk_size for i in range(desired_num_chunks + 1)]
chunk_boundaries[-1] = file_size
mini_chunk_size = 4096
for bi in range(1, len(chunk_boundaries) - 1):
pos = chunk_boundaries[bi]
file.seek(pos)
while True:
mini_chunk = file.read(mini_chunk_size)
if not mini_chunk: # EOF
chunk_boundaries[bi] = file_size
break
found_at = mini_chunk.find(split_special_token)
if found_at != -1:
chunk_boundaries[bi] = pos + found_at
break
pos += mini_chunk_size
return sorted(set(chunk_boundaries))
def _process_chunk(self, args):
"""Worker: tokenize file chunk and count token frequencies."""
filename, start, end = args
counts = defaultdict(int)
with open(filename, "rb") as f:
f.seek(start)
chunk = f.read(end - start).decode("utf-8", errors="ignore")
docs = re.split(re.escape(self.SPECIAL_TOKEN), chunk)
for doc in docs:
for match in self.TOKENIZER.finditer(doc):
token = match.group()
counts[token] += 1
return counts
def _merge_counts(self, dicts):
merged = defaultdict(int)
for d in dicts:
for k, v in d.items():
merged[k] += v
return merged
def run_pre_tokenization_parallel(self, filename: str, num_processes: int = 4):
"""Parallel pre-tokenization across file chunks."""
with open(filename, "rb") as f:
boundaries = self.find_chunk_boundaries(
f, num_processes, self.SPECIAL_TOKEN.encode("utf-8")
)
args = [(filename, s, e) for s, e in zip(boundaries[:-1], boundaries[1:])]
with Pool(num_processes) as pool:
partial_counts = pool.map(self._process_chunk, args)
return self._merge_counts(partial_counts)
# ---------- Training ----------
def get_pair_counts(self, freqs: dict[tuple[tuple[int, ...]], int]) -> dict[tuple[int, int], int]:
"""Count frequency of all adjacent byte pairs."""
pair_counts = defaultdict(int)
for sequence, count in freqs.items():
for i in range(len(sequence) - 1):
pair_counts[(sequence[i], sequence[i + 1])] += count
return pair_counts
def train(self, filename: str, vocab_size: int, num_processes: int = 4):
"""Train BPE merges and vocab from corpus."""
pre_token_counts = self.run_pre_tokenization_parallel(filename, num_processes)
# Initialize vocab with byte sequences
freqs: dict[tuple[tuple[int, ...]], int] = {}
for token, count in pre_token_counts.items():
token_bytes = token.encode("utf-8")
token_seq = tuple((b,) for b in token_bytes)
freqs[token_seq] = count
for b in token_seq:
if b not in self.vocab:
self.vocab[b] = self.next_id
self.next_id += 1
# Iteratively learn merges
while len(self.vocab) < vocab_size:
pair_counts = self.get_pair_counts(freqs)
if not pair_counts:
break
max_pair = max(pair_counts.items(), key=lambda x: (x[1], x[0]))[0]
merged_token = max_pair[0] + max_pair[1]
if merged_token in self.vocab:
continue
self.vocab[merged_token] = self.next_id
self.next_id += 1
self.merges.append(max_pair)
# Replace max pair in sequences
new_freqs = {}
for seq, freq in freqs.items():
new_seq = []
i = 0
while i < len(seq):
if i < len(seq) - 1 and (seq[i], seq[i + 1]) == max_pair:
new_seq.append(merged_token)
i += 2
else:
new_seq.append(seq[i])
i += 1
new_freqs[tuple(new_seq)] = freq
freqs = new_freqs
# Build inverse vocab for decoding
self.inv_vocab = {idx: tok for tok, idx in self.vocab.items()}
# ---------- Encoding / Decoding ----------
def encode(self, text: str) -> list[int]:
"""Encode text into BPE token IDs."""
pre_tokens = re.findall(self.PAT, text)
token_ids = []
merge_set = set(self.merges)
for token in pre_tokens:
byte_seq = [(b,) for b in token.encode("utf-8")]
while True:
merged = False
i = 0
new_seq = []
while i < len(byte_seq):
if i < len(byte_seq) - 1 and (byte_seq[i], byte_seq[i+1]) in merge_set:
new_seq.append(byte_seq[i] + byte_seq[i+1])
i += 2
merged = True
else:
new_seq.append(byte_seq[i])
i += 1
byte_seq = new_seq
if not merged:
break
token_ids.extend(self.vocab[tok] for tok in byte_seq)
return token_ids
def decode(self, token_ids: list[int]) -> str:
"""Decode token IDs back into string."""
byte_seq = []
for tid in token_ids:
tok_bytes = self.inv_vocab[tid]
byte_seq.extend(tok_bytes if isinstance(tok_bytes, tuple) else [tok_bytes])
# Convert list of ints back into bytes → decode utf-8
return bytes(byte_seq).decode("utf-8", errors="ignore")