File size: 10,366 Bytes
3b2d368 | 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 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 | from pathlib import Path
import json
import os
from datetime import datetime
from tqdm import tqdm
import numpy as np
import torch
from torch.utils.data import Dataset
class DiskDataset(Dataset):
def __init__(self, file_path, max_seq_len, stride_fraction=None, allow_cycling=False):
"""
Args:
file_path: path to the .bin file
max_seq_len:
- 在 stride_fraction=-1 (预填充模式) 下,必须与生成数据时的 pad_to_length 一致。
- 在普通模式下,表示窗口大小。
stride_fraction:
- If > 0: Sliding window (standard pre-training).
- If == -1: Pre-padded Sample Mode. Assumes data on disk is already
padded to blocks of size `max_seq_len`.
"""
self.file_path = Path(file_path).resolve()
assert self.file_path.is_file(), f"File not found: {self.file_path}"
self.max_seq_len = max_seq_len
self.stride_fraction = stride_fraction if stride_fraction is not None else 1.0
# import pdb
# pdb.set_trace()
# Determine mode
self.pre_padded_mode = (self.stride_fraction == -1)
self.data = np.memmap(self.file_path, dtype="int32", mode="r")
self.file_size = len(self.data)
if self.pre_padded_mode:
# === Mode: Pre-padded Fixed Blocks ===
# 文件大小必须是 max_seq_len 的整数倍
if self.file_size % self.max_seq_len != 0:
print(f"Warning: File size ({self.file_size}) is not a multiple of max_seq_len ({self.max_seq_len}). "
f"Last partial sample might be ignored or dataset might be corrupted.")
self.n_samples = self.file_size // self.max_seq_len
self.stride = self.max_seq_len # Stride equals length in this mode
else:
# === Mode: Sliding Window ===
self.stride = int(self.max_seq_len * self.stride_fraction)
self.n_samples = 1 + max(0, (self.file_size - self.max_seq_len) // self.stride)
self.allow_cycling = allow_cycling and not self.pre_padded_mode
def __len__(self):
return self.n_samples
def get_token_count(self):
return self.file_size
def __getitem__(self, idx):
if self.allow_cycling:
idx = idx % self.n_samples
# 无论是预填充模式还是滑动窗口模式,读取逻辑其实是一样的:
# 从 idx * stride 开始,读取 max_seq_len 长度
start = idx * self.stride
end = start + self.max_seq_len
# Create a copy to return a writeable tensor
seq = np.array(self.data[start:end], dtype=np.int32, copy=True)
return torch.from_numpy(seq).long()
@staticmethod
def generate_bin(
dataset_iterator,
tokenizer,
output_path,
add_eos=True,
column="text",
token_limit=None,
metadata_path=None,
pad_to_length=None,
pad_token_id=-100,
append: bool = False, # ✅ 新增
existing_metadata_path=None, # ✅ 可选:如果 append 时 metadata 分开存
):
output_path = Path(output_path)
output_dir = output_path.parent
output_dir.mkdir(parents=True, exist_ok=True)
def tokenize_fn(text):
ids = tokenizer.encode(text)
if add_eos:
ids.append(tokenizer.eos_token_id)
return ids
print(f"Building binary file at: {output_path}")
if pad_to_length is not None:
print(f"Mode: Fixed Block Size (Padded/Truncated to {pad_to_length})")
else:
print(f"Mode: Continuous Stream (Packed)")
dtype = np.int32
bytes_per_token = np.dtype(dtype).itemsize
# ====== ✅ append 支持:决定初始 pos、allocated ======
pos = 0
allocated = 0
mm = None
# 如果 append=True 且文件存在,就从末尾继续写
if append and output_path.exists():
file_bytes = output_path.stat().st_size
if file_bytes % bytes_per_token != 0:
raise ValueError(
f"Corrupted bin? File bytes {file_bytes} not divisible by token size {bytes_per_token}"
)
pos = file_bytes // bytes_per_token
allocated = pos # 先按当前大小映射,后面需要再 grow
if allocated == 0:
# 空文件当作新建
append = False
else:
mm = np.memmap(output_path, dtype=dtype, mode="r+", shape=(allocated,))
print(f"[INFO] Appending to existing file. Current tokens on disk: {pos}")
else:
# 原逻辑:覆盖重写
if output_path.exists():
output_path.unlink()
# tqdm
unit = "samples"
pbar = tqdm(total=None, unit=unit)
# Initial allocation size (in tokens)
initial_alloc_tokens = 1_000_000
if pad_to_length:
initial_alloc_tokens = (initial_alloc_tokens // pad_to_length) * pad_to_length
def _grow(new_alloc_tokens):
nonlocal mm, allocated
if mm is not None:
mm.flush()
del mm
mm = None
new_bytes = new_alloc_tokens * bytes_per_token
with open(output_path, "a+b") as f:
if new_bytes > 0:
f.seek(new_bytes - 1)
f.write(b"\0")
f.flush()
os.fsync(f.fileno())
allocated = new_alloc_tokens
mm = np.memmap(output_path, dtype=dtype, mode="r+", shape=(allocated,))
# ✅ 如果不是 append(或 append 但文件为空),创建初始空间
if mm is None:
_grow(initial_alloc_tokens)
else:
# ✅ append 时:如果后续要写,仍需要预留增长空间
# 这里不马上 grow,等需要的时候再 grow
pass
# ====== ✅ 如果 append,要读入已有 metadata 以便累计 ======
prev_total_tokens = 0
prev_total_samples = 0
prev_pad_to_length = None
meta_path_to_read = None
if append:
# 1) 优先从 existing_metadata_path 读取
if existing_metadata_path is not None and Path(existing_metadata_path).exists():
meta_path_to_read = Path(existing_metadata_path)
# 2) 否则从 metadata_path 读取(如果你一直写同一个 metadata)
elif metadata_path is not None and Path(metadata_path).exists():
meta_path_to_read = Path(metadata_path)
if meta_path_to_read is not None:
try:
with open(meta_path_to_read, "r", encoding="utf-8") as f:
prev = json.load(f)
prev_total_tokens = int(prev.get("total_tokens", 0))
prev_total_samples = int(prev.get("total_samples", 0))
prev_pad_to_length = prev.get("pad_to_length", None)
except Exception as e:
print(f"[WARN] Failed to read previous metadata for append: {e}")
# pad_to_length 一致性检查(很重要)
if append and prev_pad_to_length != pad_to_length:
raise ValueError(
f"pad_to_length mismatch when appending: previous={prev_pad_to_length}, new={pad_to_length}"
)
# ====== 主写入循环 ======
total_tokens_written = 0
sample_count = 0
done = False
for example in dataset_iterator:
if done:
break
ids = tokenize_fn(example[column])
if pad_to_length is not None:
if len(ids) > pad_to_length:
ids = ids[:pad_to_length]
if len(ids) < pad_to_length:
ids.extend([pad_token_id] * (pad_to_length - len(ids)))
arr = np.asarray(ids, dtype=dtype)
else:
if not ids:
continue
arr = np.asarray(ids, dtype=dtype)
# token_limit(如果你仍然想限制“本次追加写入”的 token 数)
if token_limit is not None:
if total_tokens_written + arr.size > token_limit:
done = True
if pad_to_length is None:
remaining = token_limit - total_tokens_written
arr = arr[:remaining]
else:
break
needed = pos + arr.size
if needed > allocated:
new_alloc = max(max(allocated * 2, 1_000_000), needed)
if pad_to_length:
new_alloc = ((new_alloc + pad_to_length - 1) // pad_to_length) * pad_to_length
_grow(new_alloc)
mm[pos:pos + arr.size] = arr
pos += arr.size
total_tokens_written += arr.size
sample_count += 1
pbar.update(1)
pbar.close()
if mm is not None:
mm.flush()
del mm
mm = None
# 截断到精确大小(append 时也一样需要)
with open(output_path, "r+b") as f:
f.truncate(pos * bytes_per_token)
f.flush()
os.fsync(f.fileno())
# ====== ✅ 写 metadata:累计总量 ======
if metadata_path is not None:
meta = {
"last_modified": datetime.now().isoformat(),
"total_tokens": int(prev_total_tokens + total_tokens_written),
"total_samples": int(prev_total_samples + sample_count),
"pad_to_length": pad_to_length,
"dtype": str(np.dtype(dtype)),
"append": bool(append),
}
with open(metadata_path, "w", encoding="utf-8") as f:
json.dump(meta, f, indent=2)
print(f"✅ Wrote {total_tokens_written} tokens to {output_path} (int32)")
if append:
print(f"✅ Total tokens in file now ≈ {prev_total_tokens + total_tokens_written}")
return total_tokens_written
|