File size: 17,261 Bytes
d7d2fb2
 
 
 
 
 
 
 
1b2e4da
a050405
d7d2fb2
 
 
ad359f7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d7d2fb2
 
 
1b2e4da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d7d2fb2
 
 
 
 
 
ad359f7
 
 
 
 
 
 
 
 
d7d2fb2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ad359f7
1b2e4da
 
a050405
 
d7d2fb2
326b359
ad359f7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1b2e4da
d7d2fb2
 
1b2e4da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d7d2fb2
 
1b2e4da
a050405
 
 
 
 
 
 
 
 
 
 
d7d2fb2
 
 
a050405
d7d2fb2
 
 
 
 
 
 
1b2e4da
6b1c605
d7d2fb2
 
 
 
 
 
 
 
 
 
 
 
ad359f7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
import tiktoken
import torch
from torch.utils.data import Dataset, DataLoader
from typing import Tuple, Optional, Literal, List
from pathlib import Path
from tqdm import tqdm
import mmap
import numpy as np
import os
import json

from model import ModelArgs

# Turkish Tokenizer support
try:
    from turkish_tokenizer import TurkishTokenizer as TurkishTokenizerBase
    TURKISH_TOKENIZER_AVAILABLE = True
except ImportError:
    TURKISH_TOKENIZER_AVAILABLE = False
    TurkishTokenizerBase = None

#####################################
# TURKISH TOKENIZER WRAPPER
#####################################
class TurkishTokenizerWrapper:
    def __init__(self):
        if not TURKISH_TOKENIZER_AVAILABLE:
            raise ImportError(
                "turkish-tokenizer package is not installed. "
                "Install it with: pip install turkish-tokenizer"
            )
        self.tokenizer = TurkishTokenizerBase()
        self.name = "turkish-tokenizer"

    def encode(self, text: str, allowed_special: Optional[set] = None) -> List[int]:
        return self.tokenizer.encode(text)

    def decode(self, tokens: List[int]) -> str:
        return self.tokenizer.decode(tokens)

    @property
    def n_vocab(self) -> int:
        """Get vocabulary size"""
        return self.tokenizer.vocab_size

    @property
    def max_token_value(self) -> int:
        """Get maximum token value"""
        return self.n_vocab - 1


#####################################
# DATA
#####################################

class MemoryEfficientTextDataset(Dataset):
    """
    Memory-efficient dataset that tokenizes on-the-fly from disk.
    Instead of loading all data into RAM, it:
    1. Memory-maps the text file
    2. Pre-computes line offsets for fast random access
    3. Tokenizes only the required chunks during __getitem__
    """
    def __init__(self, file_path: str, tokenizer, args: ModelArgs, stride: Optional[int] = None, max_samples: Optional[int] = None):
        self.file_path = Path(file_path)
        self.tokenizer = tokenizer
        self.max_seq_len = args.max_seq_len
        self.stride = stride if stride is not None else self.max_seq_len // 2

        if not self.file_path.exists():
            raise FileNotFoundError(f"File not found: {self.file_path}")

        print(f"📝 Creating memory-efficient dataset from {self.file_path.name}...")

        # Get file size
        file_size = self.file_path.stat().st_size
        print(f"   File size: {file_size / 1024**2:.1f} MB")

        # Pre-tokenize to get sample count (lightweight - just count)
        self._count_samples(max_samples)

        print(f"✅ Dataset ready with {len(self.samples)} samples (using lazy loading)")

    def _count_samples(self, max_samples: Optional[int]):
        """Count how many samples we can create without loading everything"""
        # Read file in chunks to estimate token count
        chunk_size = 1024 * 1024  # 1MB chunks
        total_tokens = 0

        with open(self.file_path, 'r', encoding='utf-8') as f:
            while True:
                chunk = f.read(chunk_size)
                if not chunk:
                    break
                # Quick estimate: ~4 chars per token for most languages
                total_tokens += len(chunk) // 4

        # Calculate number of samples
        num_samples = max((total_tokens - self.max_seq_len - 1) // self.stride, 1)

        if max_samples:
            num_samples = min(num_samples, max_samples)

        # Store sample metadata (start positions in tokens)
        self.samples = list(range(num_samples))
        self.estimated_total_tokens = total_tokens

    def _get_text_chunk(self, token_start: int) -> str:
        """Get a chunk of text starting from approximate token position"""
        # Estimate byte position (rough: 4 chars per token, 1 byte per char for ASCII/UTF-8)
        approx_byte_pos = token_start * 4
        chunk_size = (self.max_seq_len + 1) * 8  # Read extra for safety

        with open(self.file_path, 'r', encoding='utf-8', errors='ignore') as f:
            f.seek(max(0, approx_byte_pos - 100))  # Seek with small buffer
            return f.read(chunk_size)

    def __len__(self):
        return len(self.samples)

    def __getitem__(self, idx) -> Tuple[torch.Tensor, torch.Tensor]:
        """Tokenize on-the-fly for this specific sample"""
        # Calculate token position
        token_start = idx * self.stride

        # Get text chunk from file
        text_chunk = self._get_text_chunk(token_start)

        # Tokenize
        try:
            if hasattr(self.tokenizer, 'encode'):
                tokens = self.tokenizer.encode(text_chunk, allowed_special={"<|endoftext|>"})
            else:
                tokens = self.tokenizer.encode(text_chunk)
        except:
            # Fallback for Turkish tokenizer
            tokens = self.tokenizer.encode(text_chunk)

        # Ensure we have enough tokens
        if len(tokens) < self.max_seq_len + 1:
            # Pad with zeros if needed
            tokens = tokens + [0] * (self.max_seq_len + 1 - len(tokens))

        # Create input/target pair
        input_ids = torch.tensor(tokens[:self.max_seq_len], dtype=torch.long)
        target_ids = torch.tensor(tokens[1:self.max_seq_len + 1], dtype=torch.long)

        return input_ids, target_ids


class TextDataset(Dataset):
    def __init__(self, txt: str, tokenizer, args: ModelArgs, stride: Optional[int] = None, max_samples: Optional[int] = None):
        self.max_seq_len = args.max_seq_len
        self.stride = stride if stride is not None else self.max_seq_len // 2
        
        # Handle file paths efficiently with memory mapping
        # Check if txt is a file path (avoid Path().exists() for long strings)
        try:
            path = Path(txt)
            if len(txt) < 4096 and path.exists():  # Reasonable path length check
                text_content = self._read_file_mmap(txt)
            else:
                text_content = txt
        except (OSError, ValueError):
            # If Path() fails or string is too long, treat as raw text
            text_content = txt
        
        # Validate input
        if not text_content or len(text_content.strip()) < self.max_seq_len:
            raise ValueError(f"Text too short. Need at least {self.max_seq_len} chars, got {len(text_content)}")
        
        print(f"📝 Tokenizing {len(text_content):,} characters...")
        
        # Tokenize with progress bar for large texts
        token_ids = self._tokenize_with_progress(tokenizer, text_content)
        
        # Create sliding windows with vectorized operations
        self.samples = self._create_sliding_windows(token_ids, max_samples)
        
        print(f"✅ Created {len(self.samples)} training samples")

    def _read_file_mmap(self, file_path: str) -> str:
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                with mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mm:
                    return mm.read().decode('utf-8', errors='ignore')
        except Exception as e:
            raise RuntimeError(f"Failed to read file {file_path}: {e}")

    def _tokenize_with_progress(self, tokenizer, text: str) -> List[int]:
        # Process in chunks for memory efficiency
        chunk_size = 10_000_000  # 10MB chunks
        tokens = []
        
        if len(text) > chunk_size:
            # Process large texts in chunks
            pbar = tqdm(total=len(text), desc="Tokenizing", unit="char")
            for i in range(0, len(text), chunk_size):
                chunk = text[i:i + chunk_size]
                chunk_tokens = tokenizer.encode(chunk, allowed_special={"<|endoftext|>"})
                tokens.extend(chunk_tokens)
                pbar.update(len(chunk))
            pbar.close()
        else:
            # Single pass for smaller texts
            tokens = tokenizer.encode(text, allowed_special={"<|endoftext|>"})
        
        if not tokens:
            raise ValueError("No tokens generated from input text")
            
        return tokens

    def _create_sliding_windows(self, token_ids: List[int], max_samples: Optional[int]) -> torch.Tensor:
        if len(token_ids) < self.max_seq_len + 1:
            raise ValueError(f"Not enough tokens. Need {self.max_seq_len + 1}, got {len(token_ids)}")
        
        # Convert to numpy for faster slicing
        tokens_array = np.array(token_ids, dtype=np.int64)
        
        # Calculate number of windows
        num_windows = (len(tokens_array) - self.max_seq_len - 1) // self.stride + 1
        
        if max_samples:
            num_windows = min(num_windows, max_samples)
        
        # Pre-allocate tensors
        inputs = torch.zeros(num_windows, self.max_seq_len, dtype=torch.long)
        targets = torch.zeros(num_windows, self.max_seq_len, dtype=torch.long)
        
        # Fill tensors efficiently
        for i in range(num_windows):
            start = i * self.stride
            inputs[i] = torch.from_numpy(tokens_array[start:start + self.max_seq_len])
            targets[i] = torch.from_numpy(tokens_array[start + 1:start + self.max_seq_len + 1])
        
        # Stack into pairs (more memory efficient than separate lists)
        self.samples = torch.stack([inputs, targets], dim=1)
        
        return self.samples

    def __len__(self):
        return len(self.samples)

    def __getitem__(self, idx) -> Tuple[torch.Tensor, torch.Tensor]:
        """Return (input_ids, target_ids) tuple"""
        return self.samples[idx, 0], self.samples[idx, 1]


def create_dataloader(
    txt: str,
    args: ModelArgs,
    stride: Optional[int] = None,
    shuffle: bool = True,
    drop_last: bool = True,
    num_workers: int = 0,
    pin_memory: bool = True,
    persistent_workers: bool = False,
    max_samples: Optional[int] = None,
    use_turkish_tokenizer: bool = True,
    use_memory_efficient: bool = True,  # NEW: Use memory-efficient loading by default
    is_val: bool = True 

) -> DataLoader:

    # Select tokenizer based on user preference
    if use_turkish_tokenizer:
        if not TURKISH_TOKENIZER_AVAILABLE:
            raise ImportError(
                "Turkish tokenizer requested but not available. "
                "Install it with: pip install turkish-tokenizer"
            )
        tokenizer = TurkishTokenizerWrapper()
        print(f"🇹🇷 Using Turkish Tokenizer (vocab size: {tokenizer.n_vocab:,})")
    else:
        # Use the best default tokenizer for your setup
        # tiktoken's gpt2 is fast, well-tested, and has reasonable vocab size (~50k)
        # For multilingual or code, consider "cl100k_base" or "o200k_base"
        tokenizer_name = getattr(args, "tokenizer_name", "gpt2")
        tokenizer = tiktoken.get_encoding(tokenizer_name)
        print(f"📚 Using tiktoken tokenizer: {tokenizer_name} (vocab size: {tokenizer.n_vocab:,})")

    # Create dataset with size validation
    try:
        # Check if txt is a file path
        is_file_path = False
        try:
            path = Path(txt)
            if len(txt) < 4096 and path.exists():
                is_file_path = True
        except (OSError, ValueError):
            pass

        # Use memory-efficient dataset for file paths
        if use_memory_efficient and is_file_path:
            print(f"💾 Using memory-efficient dataset (lazy loading from disk)")
            dataset = MemoryEfficientTextDataset(
                file_path=txt,
                tokenizer=tokenizer,
                args=args,
                stride=stride,
                max_samples=max_samples
            )
        else:
            # Use original dataset (loads everything into RAM)
            print(f"⚠️  Using in-memory dataset (loads all data into RAM)")
            dataset = TextDataset(
                txt=txt,
                tokenizer=tokenizer,
                args=args,
                stride=stride,
                max_samples=max_samples
            )
    except Exception as e:
        raise RuntimeError(f"Failed to create dataset: {e}")

    config_path = Path("config.json")
    
    with open(config_path,"r") as f:
        config = json.load(f)
        val_batch_size = config["model"]["max_batch_size"] #* config["training"].get("val_batch_size_multiplier", 4)

    if is_val:
        batch_size = val_batch_size
    else:
        batch_size = args.max_batch_size

    # Create DataLoader with optimized settings
    dataloader = DataLoader(
        dataset,
        batch_size=batch_size,
        shuffle=shuffle,
        drop_last=drop_last,
        num_workers=num_workers,
        pin_memory=pin_memory,
        persistent_workers=persistent_workers if num_workers > 0 else False,
        prefetch_factor=2 if num_workers > 0 else None,
    )

    return dataloader, tokenizer


# Convenience function for downloading sample data
def get_sample_data(url: str = "https://raw.githubusercontent.com/karpathy/char-rnn/master/data/tinyshakespeare/input.txt") -> str:
    """Download sample text data for testing"""
    try:
        import requests
        response = requests.get(url)
        response.raise_for_status()
        return response.text
    except Exception as e:
        print(f"⚠️  Could not download sample data: {e}")
        return ""
    

if __name__ == "__main__":
    print("=" * 60)
    print("TOKENIZER TESTING")
    print("=" * 60)

    # Choose which tokenizer to test
    USE_TURKISH = True  # Set to False to test tiktoken instead

    if USE_TURKISH and TURKISH_TOKENIZER_AVAILABLE:
        print("\n🇹🇷 Testing Turkish Tokenizer")
        tokenizer = TurkishTokenizerWrapper()
        print(f"📚 Tokenizer: {tokenizer.name}")
        print(f"📊 Vocabulary Size: {tokenizer.n_vocab:,}")
        print(f"📝 Max Token Value: {tokenizer.max_token_value:,}")
    else:
        # Test different tokenizers
        tokenizer_name = "gpt2"  # Change to "cl100k_base" or "o200k_base" to test others
        tokenizer = tiktoken.get_encoding(tokenizer_name)

        print(f"\n📚 Tokenizer: {tokenizer_name}")
        print(f"📊 Vocabulary Size: {tokenizer.n_vocab:,}")
        print(f"📝 Max Token Value: {tokenizer.max_token_value:,}")
        print(f"🔤 Name: {tokenizer.name}")

    # Test encoding/decoding
    if USE_TURKISH and TURKISH_TOKENIZER_AVAILABLE:
        test_samples = [
            "Merhaba Dünya!",
            "İstanbul'da yaşıyorum ve Türkçe dilini öğreniyorum.",
            "Kitap okumak çok güzeldir ve bilgi verir.",
            "Türkiye Cumhuriyeti'nin başkenti Ankara'dır.",
            "Yapay zeka ve makine öğrenmesi teknolojileri gelişiyor.",
        ]
    else:
        test_samples = [
            "Hello, world!",
            "The quick brown fox jumps over the lazy dog.",
            "Machine learning is fascinating.",
            "print('Hello, World!')",  # Code sample
            "日本語のテキスト",  # Non-English
        ]

    print("\n" + "=" * 60)
    print("ENCODING EXAMPLES")
    print("=" * 60)

    for text in test_samples:
        tokens = tokenizer.encode(text)
        decoded = tokenizer.decode(tokens)
        print(f"\nText: {text}")
        print(f"Tokens ({len(tokens)}): {tokens}")
        print(f"Token range: [{min(tokens)}, {max(tokens)}]")
        print(f"Decoded: {decoded}")

    # Test with actual data
    print("\n" + "=" * 60)
    print("DATALOADER TESTING")
    print("=" * 60)

    sample_text = get_sample_data()
    if sample_text:
        print(f"\n📄 Sample text length: {len(sample_text):,} characters")

        # Tokenize sample
        if USE_TURKISH and TURKISH_TOKENIZER_AVAILABLE:
            full_tokens = tokenizer.encode(sample_text)
        else:
            full_tokens = tokenizer.encode(sample_text, allowed_special={"<|endoftext|>"})

        print(f"🔢 Total tokens: {len(full_tokens):,}")
        print(f"📈 Unique tokens used: {len(set(full_tokens)):,}")
        print(f"📊 Vocabulary coverage: {len(set(full_tokens)) / tokenizer.n_vocab * 100:.2f}%")

        # Create dataloader
        args = ModelArgs(max_seq_len=128, max_batch_size=16)
        dataloader = create_dataloader(
            sample_text,
            args,
            num_workers=0,
            max_samples=100,
            use_turkish_tokenizer=USE_TURKISH and TURKISH_TOKENIZER_AVAILABLE
        )

        print(f"\n⚙️  DataLoader Config:")
        print(f"   Sequence length: {args.max_seq_len}")
        print(f"   Batch size: {args.max_batch_size}")
        print(f"   Total batches: {len(dataloader)}")

        # Test first batch
        for batch_idx, (input_ids, target_ids) in enumerate(dataloader):
            print(f"\n🎯 Batch {batch_idx}:")
            print(f"   input_ids shape: {input_ids.shape}")
            print(f"   target_ids shape: {target_ids.shape}")
            print(f"   input_ids range: [{input_ids.min().item()}, {input_ids.max().item()}]")
            print(f"   Sample input (first 10 tokens): {input_ids[0, :10].tolist()}")
            print(f"   Decoded: {tokenizer.decode(input_ids[0, :10].tolist())}")
            break

    print("\n" + "=" * 60)
    print("✅ Testing complete!")
    print("=" * 60)