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