thefinalboss commited on
Commit
ec5053e
Β·
verified Β·
1 Parent(s): a37c0fc

Upload runpod_train_1b.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. runpod_train_1b.py +1393 -0
runpod_train_1b.py ADDED
@@ -0,0 +1,1393 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ CogNet-1B β€” RunPod All-in-One Training Script (AICL + HuggingFace Datasets Edition)
4
+ =============================================================
5
+ 1. Downloads real HuggingFace datasets (wikitext, codeparrot, fineweb, oscar, c4, alpaca, the-stack)
6
+ 2. Downloads pre-tokenized data from CogNet HF repo (if available)
7
+ 3. Clones AICL repo from GitHub (github.com/AFKmoney/AICL)
8
+ 4. Converts AICL datasets + examples + source + spec to training tokens
9
+ 5. Tokenizes HF scripts + config files
10
+ 6. Generates supplementary synthetic data (code + English + French)
11
+ 7. Merges ALL data into final training dataset
12
+ 8. Builds the 1B model (~1.06B params)
13
+ 9. Trains with BF16 mixed precision on RTX PRO 6000
14
+ 10. Auto-saves checkpoints to network volume
15
+ 11. Auto-pushes to HuggingFace when training completes
16
+ 12. Graceful shutdown on SIGTERM/SIGINT (saves checkpoint)
17
+ 13. Auto-resume from latest checkpoint if found
18
+
19
+ Data mix: ~60% HuggingFace datasets + 15% AICL (10x repeated) + 5% scripts + 20% synthetic
20
+
21
+ Usage:
22
+ python3 runpod_train_1b.py [--max-steps 200000] [--batch-size 8] [--skip-data-prep]
23
+
24
+ CHECKPOINTS ARE SAVED TO /workspace/CogNet/checkpoints_1b/
25
+ This should be on a RunPod Network Volume so data survives pod restarts!
26
+ """
27
+
28
+ import os
29
+ import sys
30
+ import time
31
+ import math
32
+ import json
33
+ import random
34
+ import string
35
+ import logging
36
+ import argparse
37
+ import signal
38
+ import subprocess
39
+ from pathlib import Path
40
+
41
+ import torch
42
+ import torch.nn as nn
43
+ import torch.nn.functional as F
44
+ from torch.utils.data import Dataset, DataLoader
45
+
46
+ # ──────────────────────────────────────────────────────────
47
+ # Paths β€” /workspace is the RunPod Network Volume
48
+ # ──────────────────────────────────────────────────────────
49
+ WORKSPACE = os.environ.get('COGNET_WORKSPACE', '/workspace/CogNet')
50
+ DATA_DIR = os.path.join(WORKSPACE, 'data_1b')
51
+ CKPT_DIR = os.path.join(WORKSPACE, 'checkpoints_1b')
52
+ LOG_FILE = os.path.join(WORKSPACE, 'train_1b.log')
53
+ TOKENIZER_PATH = os.path.join(WORKSPACE, 'tokenizer_v3.json')
54
+ HF_REPO = 'thefinalboss/CogNet-1B'
55
+ HF_TOKEN = os.environ.get('HF_TOKEN', '')
56
+
57
+ # ──────────────────────────────────────────────────────────
58
+ # Logging
59
+ # ──────────────────────────────────────────────────────────
60
+ os.makedirs(WORKSPACE, exist_ok=True)
61
+ os.makedirs(CKPT_DIR, exist_ok=True)
62
+ os.makedirs(DATA_DIR, exist_ok=True)
63
+
64
+ logging.basicConfig(
65
+ level=logging.INFO,
66
+ format='%(asctime)s [%(levelname)s] %(message)s',
67
+ handlers=[
68
+ logging.StreamHandler(sys.stdout),
69
+ logging.FileHandler(LOG_FILE, mode='a'),
70
+ ],
71
+ )
72
+ log = logging.getLogger('cognet1b')
73
+
74
+ # ──────────────────────────────────────────────────────────
75
+ # Graceful shutdown β€” save checkpoint on SIGTERM/SIGINT
76
+ # ──────────────────────────────────────────────────────────
77
+ shutdown_requested = False
78
+ def handle_signal(signum, frame):
79
+ global shutdown_requested
80
+ log.warning(f'Received signal {signum}, will save checkpoint and exit after current step...')
81
+ shutdown_requested = True
82
+
83
+ signal.signal(signal.SIGTERM, handle_signal)
84
+ signal.signal(signal.SIGINT, handle_signal)
85
+
86
+
87
+ # ═══════════════════════════════════════════════════════════
88
+ # AICL Repo URL
89
+ # ═══════════════════════════════════════════════════════════
90
+ AICL_REPO_URL = 'https://github.com/AFKmoney/AICL.git'
91
+ AICL_LOCAL = os.path.join(WORKSPACE, 'aicl_repo')
92
+ AICL_REPEAT = int(os.environ.get('AICL_REPEAT', '10'))
93
+
94
+
95
+ # ═══════════════════════════════════════════════════════════
96
+ # Step 1: Clone AICL repo from GitHub
97
+ # ═══════════════════════════════════════════════════════════
98
+ def clone_aicl_repo():
99
+ """Clone the AICL GitHub repository for dataset conversion."""
100
+ if os.path.isdir(os.path.join(AICL_LOCAL, '.git')):
101
+ log.info(f'AICL repo already exists at {AICL_LOCAL}')
102
+ return
103
+ log.info('Cloning AICL repo from GitHub...')
104
+ subprocess.run(['git', 'clone', AICL_REPO_URL, AICL_LOCAL], check=True)
105
+ log.info(f'AICL repo cloned to {AICL_LOCAL}')
106
+
107
+
108
+ # ═══════════════════════════════════════════════════════════
109
+ # Step 2: Extract text from AICL repo
110
+ # ═══════════════════════════════════════════════════════════
111
+ def extract_aicl_jsonl(repo_path):
112
+ """Extract text from JSONL dataset files."""
113
+ texts = []
114
+ datasets_dir = os.path.join(repo_path, 'datasets')
115
+ if not os.path.isdir(datasets_dir):
116
+ log.warning('No datasets/ directory in AICL repo')
117
+ return texts
118
+
119
+ import glob as glob_mod
120
+ jsonl_files = sorted(glob_mod.glob(os.path.join(datasets_dir, '*.jsonl')))
121
+ log.info(f' Found {len(jsonl_files)} JSONL files')
122
+
123
+ total = 0
124
+ for jf in jsonl_files:
125
+ count = 0
126
+ with open(jf, 'r', encoding='utf-8') as f:
127
+ for line in f:
128
+ line = line.strip()
129
+ if not line:
130
+ continue
131
+ try:
132
+ entry = json.loads(line)
133
+ except json.JSONDecodeError:
134
+ continue
135
+ if 'code' in entry:
136
+ texts.append(entry['code'])
137
+ count += 1
138
+ elif 'completion' in entry:
139
+ instr = entry.get('instruction', '')
140
+ if instr:
141
+ texts.append(f"# Instruction:\n{instr}\n\n# Completion:\n{entry['completion']}")
142
+ else:
143
+ texts.append(entry['completion'])
144
+ count += 1
145
+ if 'snippets' in entry:
146
+ for snip in entry['snippets']:
147
+ if isinstance(snip, dict) and 'completion' in snip:
148
+ texts.append(snip['completion'])
149
+ count += 1
150
+ elif isinstance(snip, str):
151
+ texts.append(snip)
152
+ count += 1
153
+ total += count
154
+ log.info(f' {os.path.basename(jf)}: {count} entries')
155
+ log.info(f' JSONL total: {total} entries')
156
+ return texts
157
+
158
+
159
+ def extract_aicl_examples(repo_path):
160
+ """Extract .aicl example files."""
161
+ import glob as glob_mod
162
+ texts = []
163
+ examples_dir = os.path.join(repo_path, 'examples')
164
+ if not os.path.isdir(examples_dir):
165
+ return texts
166
+ aicl_files = sorted(glob_mod.glob(os.path.join(examples_dir, '**/*.aicl'), recursive=True))
167
+ for af in aicl_files:
168
+ try:
169
+ with open(af, 'r', encoding='utf-8') as f:
170
+ content = f.read()
171
+ if content.strip():
172
+ texts.append(f"# === AICL Example: {os.path.basename(af)} ===\n{content}")
173
+ except Exception:
174
+ pass
175
+ log.info(f' .aicl examples: {len(texts)} files')
176
+ return texts
177
+
178
+
179
+ def extract_aicl_source(repo_path):
180
+ """Extract source code from src/, tools/, scripts/."""
181
+ import glob as glob_mod
182
+ texts = []
183
+ code_dirs = ['src', 'tools', 'scripts']
184
+ code_exts = {'.py', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.prisma'}
185
+ for cdir in code_dirs:
186
+ full_dir = os.path.join(repo_path, cdir)
187
+ if not os.path.isdir(full_dir):
188
+ continue
189
+ count = 0
190
+ for ext in code_exts:
191
+ for cf in sorted(glob_mod.glob(os.path.join(full_dir, f'**/*{ext}'), recursive=True)):
192
+ if 'node_modules' in cf or '.next' in cf or '__pycache__' in cf:
193
+ continue
194
+ try:
195
+ with open(cf, 'r', encoding='utf-8') as f:
196
+ content = f.read()
197
+ if len(content.strip()) > 50:
198
+ texts.append(f"# === Source: {os.path.relpath(cf, repo_path)} ===\n{content}")
199
+ count += 1
200
+ except Exception:
201
+ pass
202
+ log.info(f' {cdir}/: {count} files')
203
+ return texts
204
+
205
+
206
+ def extract_aicl_spec_docs(repo_path):
207
+ """Extract spec, docs, README, tests."""
208
+ import glob as glob_mod
209
+ texts = []
210
+ # Spec
211
+ for f in sorted(glob_mod.glob(os.path.join(repo_path, 'spec', '*'))):
212
+ try:
213
+ with open(f, 'r', encoding='utf-8') as fh:
214
+ content = fh.read()
215
+ if content.strip():
216
+ texts.append(f"# === AICL Spec: {os.path.relpath(f, repo_path)} ===\n{content}")
217
+ except Exception:
218
+ pass
219
+ # Docs
220
+ for f in sorted(glob_mod.glob(os.path.join(repo_path, 'docs', '*'))):
221
+ try:
222
+ with open(f, 'r', encoding='utf-8') as fh:
223
+ content = fh.read()
224
+ if content.strip():
225
+ texts.append(f"# === AICL Docs: {os.path.relpath(f, repo_path)} ===\n{content}")
226
+ except Exception:
227
+ pass
228
+ # README
229
+ readme = os.path.join(repo_path, 'README.md')
230
+ if os.path.isfile(readme):
231
+ try:
232
+ with open(readme, 'r', encoding='utf-8') as f:
233
+ texts.append(f"# === AICL README ===\n{f.read()}")
234
+ except Exception:
235
+ pass
236
+ # Tests
237
+ for f in sorted(glob_mod.glob(os.path.join(repo_path, 'tests', '*.py'))):
238
+ try:
239
+ with open(f, 'r', encoding='utf-8') as fh:
240
+ content = fh.read()
241
+ if len(content.strip()) > 100:
242
+ texts.append(f"# === AICL Tests: {os.path.relpath(f, repo_path)} ===\n{content}")
243
+ except Exception:
244
+ pass
245
+ log.info(f' Spec/docs/tests: {len(texts)} files')
246
+ return texts
247
+
248
+
249
+ # ═══════════════════════════════════════════════════════════
250
+ # Step 3: Data Preparation β€” Full Pipeline
251
+ # ═══════════════════════════════════════════════════════════
252
+ def prepare_data(skip=False):
253
+ """
254
+ Full data preparation pipeline:
255
+ A. Download real HuggingFace datasets (wikitext, code, web, French)
256
+ B. Download pre-tokenized data from CogNet HF repo (if available)
257
+ C. Clone AICL repo from GitHub β†’ convert to tokens
258
+ D. Load scripts from HuggingFace and convert to tokens
259
+ E. Generate synthetic data (code + English + French)
260
+ F. Merge everything + shuffle β†’ train_merged.pt
261
+ """
262
+ if skip:
263
+ log.info('Skipping data preparation (--skip-data-prep)')
264
+ return
265
+
266
+ # Check if merged data already exists
267
+ train_path = os.path.join(DATA_DIR, 'train_merged.pt')
268
+ if os.path.exists(train_path):
269
+ size_mb = os.path.getsize(train_path) / 1e6
270
+ log.info(f'Training data already exists: {train_path} ({size_mb:.0f} MB)')
271
+ return
272
+
273
+ # Log existing partial files (but DON'T skip β€” we may need to add HF datasets)
274
+ pt_files = list(Path(DATA_DIR).glob('*.pt'))
275
+ if pt_files:
276
+ total_mb = sum(f.stat().st_size for f in pt_files) / 1e6
277
+ log.info(f'Found {len(pt_files)} existing tokenized files ({total_mb:.0f} MB) in {DATA_DIR}')
278
+ for f in pt_files:
279
+ log.info(f' {f.name} ({f.stat().st_size / 1e6:.0f} MB)')
280
+
281
+ log.info('='*60)
282
+ log.info('DATA PREPARATION β€” Full Pipeline')
283
+ log.info('A. HuggingFace datasets (wikitext, code, web, French)')
284
+ log.info('B. CogNet HF repo data (pre-tokenized)')
285
+ log.info('C. AICL repo conversion (GitHub β†’ tokens)')
286
+ log.info('D. HuggingFace scripts β†’ tokens')
287
+ log.info('E. Synthetic data generation')
288
+ log.info('F. Merge + shuffle')
289
+ log.info('='*60)
290
+
291
+ tokenizer = CharTokenizer()
292
+ all_tensors = [] # List of tensors to merge
293
+
294
+ # ──────────────────────────────────────────────────────
295
+ # Part A: Download REAL HuggingFace datasets
296
+ # ──────────────────────────────────────────────────────
297
+ log.info('\n--- Part A: Downloading HuggingFace datasets ---')
298
+ subprocess.run([sys.executable, '-m', 'pip', 'install', 'datasets', '-q'], check=False)
299
+
300
+ hf_datasets_path = os.path.join(DATA_DIR, 'hf_datasets_tokens.pt')
301
+ if os.path.exists(hf_datasets_path):
302
+ log.info(f' HF datasets tokens already exist: {hf_datasets_path}')
303
+ t = torch.load(hf_datasets_path, map_location='cpu', weights_only=True).long()
304
+ all_tensors.append(t)
305
+ log.info(f' Loaded: {len(t):,} tokens')
306
+ else:
307
+ try:
308
+ from datasets import load_dataset
309
+
310
+ hf_ids = [] # All token IDs from HF datasets
311
+ total_chars = 0
312
+
313
+ # --- A1: Wikitext-103-raw (English Wikipedia, ~500MB text) ---
314
+ log.info(' A1: Loading wikitext-103-raw-v1...')
315
+ try:
316
+ wt = load_dataset('wikitext', 'wikitext-103-raw-v1', split='train')
317
+ wt_texts = [x['text'] for x in wt if x['text'] and len(x['text'].strip()) > 20]
318
+ wt_chars = sum(len(t) for t in wt_texts)
319
+ log.info(f' wikitext: {len(wt_texts):,} docs, {wt_chars:,} chars ({wt_chars/1e6:.0f} MB)')
320
+ for text in wt_texts:
321
+ hf_ids.extend(tokenizer.encode(text))
322
+ total_chars += wt_chars
323
+ except Exception as e:
324
+ log.warning(f' wikitext failed: {e}')
325
+
326
+ # --- A2: CodeParrot-clean (Python code) ---
327
+ log.info(' A2: Loading codeparrot/codeparrot-clean...')
328
+ try:
329
+ cp = load_dataset('codeparrot/codeparrot-clean', split='train', streaming=True)
330
+ cp_chars = 0
331
+ cp_docs = 0
332
+ for example in cp:
333
+ code = example.get('content', '') or example.get('text', '')
334
+ if len(code.strip()) > 100:
335
+ hf_ids.extend(tokenizer.encode(code))
336
+ cp_chars += len(code)
337
+ cp_docs += 1
338
+ if cp_chars > 300_000_000: # 300M chars max
339
+ break
340
+ log.info(f' codeparrot: {cp_docs:,} files, {cp_chars:,} chars ({cp_chars/1e6:.0f} MB)')
341
+ total_chars += cp_chars
342
+ except Exception as e:
343
+ log.warning(f' codeparrot failed: {e}')
344
+
345
+ # --- A3: FineWeb (high quality web text, sample 5M chars) ---
346
+ log.info(' A3: Loading HuggingFaceFW/fineweb (sample)...')
347
+ try:
348
+ fw = load_dataset('HuggingFaceFW/fineweb', split='train', streaming=True)
349
+ fw_chars = 0
350
+ fw_docs = 0
351
+ for example in fw:
352
+ text = example.get('text', '')
353
+ if len(text.strip()) > 50:
354
+ hf_ids.extend(tokenizer.encode(text))
355
+ fw_chars += len(text)
356
+ fw_docs += 1
357
+ if fw_chars > 500_000_000: # 500M chars max
358
+ break
359
+ log.info(f' fineweb: {fw_docs:,} docs, {fw_chars:,} chars ({fw_chars/1e6:.0f} MB)')
360
+ total_chars += fw_chars
361
+ except Exception as e:
362
+ log.warning(f' fineweb failed: {e}')
363
+
364
+ # --- A4: OSCAR French subset (French web text) ---
365
+ log.info(' A4: Loading oscar (French subset)...')
366
+ try:
367
+ oscar_fr = load_dataset('oscar', 'unshuffled_deduplicated_fr', split='train', streaming=True, trust_remote_code=True)
368
+ fr_chars = 0
369
+ fr_docs = 0
370
+ for example in oscar_fr:
371
+ text = example.get('text', '')
372
+ if len(text.strip()) > 50:
373
+ hf_ids.extend(tokenizer.encode(text))
374
+ fr_chars += len(text)
375
+ fr_docs += 1
376
+ if fr_chars > 100_000_000: # 100M chars max
377
+ break
378
+ log.info(f' oscar-fr: {fr_docs:,} docs, {fr_chars:,} chars ({fr_chars/1e6:.0f} MB)')
379
+ total_chars += fr_chars
380
+ except Exception as e:
381
+ log.warning(f' oscar-fr failed: {e}')
382
+
383
+ # --- A5: The Stack Smol (multi-language code) ---
384
+ log.info(' A5: Loading bigcode/the-stack-smol...')
385
+ try:
386
+ stack = load_dataset('bigcode/the-stack-smol', split='train', streaming=True, trust_remote_code=True)
387
+ stack_chars = 0
388
+ stack_docs = 0
389
+ for example in stack:
390
+ code = example.get('content', '') or example.get('text', '')
391
+ if len(code.strip()) > 100:
392
+ hf_ids.extend(tokenizer.encode(code))
393
+ stack_chars += len(code)
394
+ stack_docs += 1
395
+ if stack_chars > 200_000_000: # 200M chars max
396
+ break
397
+ log.info(f' the-stack-smol: {stack_docs:,} files, {stack_chars:,} chars ({stack_chars/1e6:.0f} MB)')
398
+ total_chars += stack_chars
399
+ except Exception as e:
400
+ log.warning(f' the-stack-smol failed: {e}')
401
+
402
+ # --- A6: Alpaca-cleaned (instruction following) ---
403
+ log.info(' A6: Loading yahma/alpaca-cleaned...')
404
+ try:
405
+ alpaca = load_dataset('yahma/alpaca-cleaned', split='train')
406
+ alp_texts = []
407
+ for x in alpaca:
408
+ instr = x.get('instruction', '')
409
+ inp = x.get('input', '')
410
+ out = x.get('output', '')
411
+ text = f"### Instruction:\n{instr}\n"
412
+ if inp:
413
+ text += f"### Input:\n{inp}\n"
414
+ text += f"### Response:\n{out}\n"
415
+ alp_texts.append(text)
416
+ alp_chars = sum(len(t) for t in alp_texts)
417
+ log.info(f' alpaca: {len(alp_texts):,} instructions, {alp_chars:,} chars')
418
+ for text in alp_texts:
419
+ hf_ids.extend(tokenizer.encode(text))
420
+ total_chars += alp_chars
421
+ except Exception as e:
422
+ log.warning(f' alpaca failed: {e}')
423
+
424
+ # --- A7: C4 (Colossal Clean Crawled Corpus, English sample) ---
425
+ log.info(' A7: Loading c4 (en, sample)...')
426
+ try:
427
+ c4 = load_dataset('c4', 'en', split='train', streaming=True)
428
+ c4_chars = 0
429
+ c4_docs = 0
430
+ for example in c4:
431
+ text = example.get('text', '')
432
+ if len(text.strip()) > 100:
433
+ hf_ids.extend(tokenizer.encode(text))
434
+ c4_chars += len(text)
435
+ c4_docs += 1
436
+ if c4_chars > 300_000_000: # 300M chars max
437
+ break
438
+ log.info(f' c4-en: {c4_docs:,} docs, {c4_chars:,} chars ({c4_chars/1e6:.0f} MB)')
439
+ total_chars += c4_chars
440
+ except Exception as e:
441
+ log.warning(f' c4 failed: {e}')
442
+
443
+ # Save all HF dataset tokens
444
+ log.info(f' Total HF datasets: {len(hf_ids):,} tokens, {total_chars:,} chars')
445
+ if hf_ids:
446
+ hf_tensor = torch.tensor(hf_ids, dtype=torch.long)
447
+ torch.save(hf_tensor, hf_datasets_path)
448
+ log.info(f' Saved HF datasets tokens: {hf_datasets_path} ({os.path.getsize(hf_datasets_path)/1e6:.0f} MB)')
449
+ all_tensors.append(hf_tensor)
450
+ del hf_ids, hf_tensor
451
+ else:
452
+ log.warning(' No HF dataset tokens collected!')
453
+
454
+ except ImportError:
455
+ log.warning(' datasets library not available, skipping HF datasets')
456
+ except Exception as e:
457
+ log.warning(f' HF datasets loading failed: {e}')
458
+
459
+ # ──────────────────────────────────────────────────────
460
+ # Part B: Download pre-tokenized data from CogNet HF repo
461
+ # ──────────────────────────────────────────────────────
462
+ log.info('\n--- Part B: Downloading CogNet HF repo data ---')
463
+ try:
464
+ from huggingface_hub import hf_hub_download, list_repo_files
465
+
466
+ # Install huggingface_hub if needed
467
+ subprocess.run([sys.executable, '-m', 'pip', 'install', 'huggingface_hub', '-q'], check=False)
468
+
469
+ # List data files in HF repo
470
+ repo_files = list_repo_files(HF_REPO, token=HF_TOKEN)
471
+ data_files = [f for f in repo_files if f.startswith('data/') and f.endswith('.pt')]
472
+ script_files = [f for f in repo_files if f.endswith('.py') or f.endswith('.json')]
473
+ log.info(f'HF repo: {len(data_files)} data files, {len(script_files)} script files')
474
+
475
+ # Download any pre-tokenized .pt data files
476
+ for df in data_files:
477
+ fname = os.path.basename(df)
478
+ local_path = os.path.join(DATA_DIR, fname)
479
+ if os.path.exists(local_path):
480
+ log.info(f' Already have {fname}, skipping')
481
+ continue
482
+ log.info(f' Downloading {df}...')
483
+ try:
484
+ hf_hub_download(HF_REPO, df, local_dir=DATA_DIR, token=HF_TOKEN)
485
+ size_mb = os.path.getsize(local_path) / 1e6
486
+ log.info(f' Downloaded {fname} ({size_mb:.0f} MB)')
487
+ except Exception as e:
488
+ log.warning(f' Failed to download {df}: {e}')
489
+
490
+ # Download scripts for tokenization
491
+ hf_scripts_dir = os.path.join(WORKSPACE, 'hf_scripts')
492
+ os.makedirs(hf_scripts_dir, exist_ok=True)
493
+ for sf in script_files:
494
+ if sf.startswith('data/'):
495
+ continue # Skip data dir scripts
496
+ try:
497
+ hf_hub_download(HF_REPO, sf, local_dir=hf_scripts_dir, token=HF_TOKEN)
498
+ except Exception:
499
+ pass
500
+ log.info(f' Downloaded scripts to {hf_scripts_dir}')
501
+
502
+ except Exception as e:
503
+ log.warning(f'HF download failed (non-fatal): {e}')
504
+ log.info('Will proceed without HF pre-tokenized data')
505
+
506
+ # Collect any downloaded .pt files (excluding hf_datasets_tokens.pt already loaded)
507
+ for pt_file in sorted(Path(DATA_DIR).glob('*.pt')):
508
+ if pt_file.name == 'hf_datasets_tokens.pt':
509
+ continue # Already loaded in Part A
510
+ if pt_file.name in ['aicl_tokens.pt', 'hf_scripts_tokens.pt', 'synthetic_tokens.pt', 'train_merged.pt']:
511
+ continue # Will be loaded later
512
+ log.info(f' Loading {pt_file.name}...')
513
+ t = torch.load(str(pt_file), map_location='cpu', weights_only=True).long()
514
+ all_tensors.append(t)
515
+ log.info(f' {pt_file.name}: {len(t):,} tokens')
516
+
517
+ # ──────────────────────────────────────────────────────
518
+ # Part C: Clone AICL repo from GitHub β†’ convert to tokens
519
+ # ──────────────────────────────────────────────────────
520
+ aicl_path = os.path.join(DATA_DIR, 'aicl_tokens.pt')
521
+ if os.path.exists(aicl_path):
522
+ log.info(f'\n--- Part C: AICL tokens already exist, loading {aicl_path} ---')
523
+ aicl_tensor = torch.load(aicl_path, map_location='cpu', weights_only=True).long()
524
+ log.info(f' AICL: {len(aicl_tensor):,} tokens ({os.path.getsize(aicl_path)/1e6:.0f} MB)')
525
+ all_tensors.append(aicl_tensor)
526
+ else:
527
+ log.info('\n--- Part C: AICL Repo Conversion ---')
528
+ clone_aicl_repo()
529
+
530
+ log.info('Extracting text from AICL repo...')
531
+ aicl_texts = []
532
+ aicl_texts.extend(extract_aicl_jsonl(AICL_LOCAL))
533
+ aicl_texts.extend(extract_aicl_examples(AICL_LOCAL))
534
+ aicl_texts.extend(extract_aicl_source(AICL_LOCAL))
535
+ aicl_texts.extend(extract_aicl_spec_docs(AICL_LOCAL))
536
+
537
+ aicl_chars = sum(len(t) for t in aicl_texts)
538
+ log.info(f'AICL extracted: {len(aicl_texts):,} chunks, {aicl_chars:,} chars ({aicl_chars/1e6:.1f} MB)')
539
+
540
+ # Repeat AICL data to increase weight in training
541
+ aicl_texts_repeated = aicl_texts * AICL_REPEAT
542
+ aicl_chars_rep = sum(len(t) for t in aicl_texts_repeated)
543
+ log.info(f'AICL after {AICL_REPEAT}x repeat: {len(aicl_texts_repeated):,} chunks, {aicl_chars_rep:,} chars ({aicl_chars_rep/1e6:.1f} MB)')
544
+
545
+ # Tokenize AICL
546
+ log.info('Tokenizing AICL data...')
547
+ aicl_ids = []
548
+ for i, text in enumerate(aicl_texts_repeated):
549
+ aicl_ids.extend(tokenizer.encode(text))
550
+ if (i + 1) % 5000 == 0:
551
+ log.info(f' AICL tokenized {i+1}/{len(aicl_texts_repeated)} chunks ({len(aicl_ids):,} tokens)')
552
+ log.info(f'AICL tokens: {len(aicl_ids):,}')
553
+
554
+ aicl_tensor = torch.tensor(aicl_ids, dtype=torch.long)
555
+ torch.save(aicl_tensor, aicl_path)
556
+ log.info(f'AICL data saved: {aicl_path} ({os.path.getsize(aicl_path)/1e6:.0f} MB)')
557
+ all_tensors.append(aicl_tensor)
558
+ del aicl_texts, aicl_texts_repeated, aicl_ids # Free memory
559
+
560
+ # ──────────────────────────────────────────────────────
561
+ # Part D: Tokenize HF scripts + config files
562
+ # ──────────────────────────────────────────────────────
563
+ log.info('\n--- Part D: HuggingFace Scripts β†’ Tokens ---')
564
+ script_texts = []
565
+ hf_scripts_dir = os.path.join(WORKSPACE, 'hf_scripts')
566
+ if os.path.isdir(hf_scripts_dir):
567
+ import glob as glob_mod
568
+ for ext in ['.py', '.json', '.md']:
569
+ for sf in sorted(glob_mod.glob(os.path.join(hf_scripts_dir, f'**/*{ext}'), recursive=True)):
570
+ if 'node_modules' in sf or '.git' in sf:
571
+ continue
572
+ try:
573
+ with open(sf, 'r', encoding='utf-8') as f:
574
+ content = f.read()
575
+ if len(content.strip()) > 50:
576
+ rel = os.path.relpath(sf, hf_scripts_dir)
577
+ script_texts.append(f"# === HF Script: {rel} ===\n{content}")
578
+ except Exception:
579
+ pass
580
+
581
+ # Also tokenize CogNet training scripts themselves (the model learns its own code!)
582
+ for script_name in ['runpod_train_1b.py', 'cognet_1b.py', 'train_1b_final.py',
583
+ 'gen_data_1b.py', 'cognet_data_prep.py', 'cognet_hf_push.py',
584
+ 'build_aicl_dataset.py']:
585
+ sp = os.path.join(hf_scripts_dir, script_name)
586
+ if os.path.exists(sp):
587
+ try:
588
+ with open(sp, 'r', encoding='utf-8') as f:
589
+ content = f.read()
590
+ if len(content.strip()) > 100:
591
+ script_texts.append(f"# === CogNet Script: {script_name} ===\n{content}")
592
+ except Exception:
593
+ pass
594
+
595
+ if script_texts:
596
+ log.info(f'Found {len(script_texts)} HF scripts to tokenize')
597
+ script_ids = []
598
+ for text in script_texts:
599
+ script_ids.extend(tokenizer.encode(text))
600
+ script_tensor = torch.tensor(script_ids, dtype=torch.long)
601
+ script_path = os.path.join(DATA_DIR, 'hf_scripts_tokens.pt')
602
+ torch.save(script_tensor, script_path)
603
+ log.info(f'HF scripts: {len(script_ids):,} tokens saved to {script_path}')
604
+ # Repeat 3x to give scripts some weight
605
+ all_tensors.append(script_tensor.repeat(3))
606
+ del script_texts, script_ids, script_tensor
607
+ else:
608
+ log.info('No HF scripts found to tokenize')
609
+
610
+ # ──────────────────────────────────────────────────────
611
+ # Part E: Synthetic data (code + English + French)
612
+ # ──────────────────────────────────────────────────────
613
+ syn_path = os.path.join(DATA_DIR, 'synthetic_tokens.pt')
614
+ if os.path.exists(syn_path):
615
+ log.info(f'\n--- Part E: Synthetic tokens already exist, loading {syn_path} ---')
616
+ syn_tensor = torch.load(syn_path, map_location='cpu', weights_only=True).long()
617
+ log.info(f' Synthetic: {len(syn_tensor):,} tokens ({os.path.getsize(syn_path)/1e6:.0f} MB)')
618
+ all_tensors.append(syn_tensor)
619
+ else:
620
+ log.info('\n--- Part E: Synthetic Data Generation ---')
621
+ target_synthetic_chars = 50_000_000 # 50M chars (reduced β€” real HF datasets provide most data now)
622
+
623
+ func_names = ['process','compute','transform','validate','parse','encode','decode',
624
+ 'train','predict','analyze','optimize','initialize','configure','execute',
625
+ 'render','compile','evaluate','generate','filter','sort','merge','split']
626
+ cls_names = ['Model','Processor','Handler','Manager','Engine','Pipeline','Service',
627
+ 'Client','Server','Agent','Router','Builder','Factory','Observer']
628
+ params = ['x','y','data','input','value','config','params','options','state','context']
629
+
630
+ py_templates = [
631
+ "def {f}({p1}, {p2}):\n \"\"\"Process {p1} with {p2}.\"\"\"\n result = {p1} + {p2}\n return result\n\n",
632
+ "class {cls}:\n def __init__(self, {p1}):\n self.{p1} = {p1}\n\n def process(self, {p2}):\n return self.{p1} * {p2}\n\n",
633
+ "for {var} in range({n}):\n {p1}({var})\n\n",
634
+ "try:\n {var} = {func}({p1})\nexcept ValueError as e:\n {var} = None\n\n",
635
+ "{var} = [{expr} for {var2} in {iter} if {cond}]\n",
636
+ "async def {f}({p1}):\n result = await process({p1})\n return result\n\n",
637
+ "import {mod}\nfrom {mod2} import {imp}\n\n{var} = {expr}\nif {cond}:\n {stmt}\n",
638
+ ]
639
+
640
+ en_sentences = [
641
+ "The quick brown fox jumps over the lazy dog. ",
642
+ "Science tells us that the universe is vast and beautiful. ",
643
+ "CogNet is a non-transformer language model with cognitive routing and memory. ",
644
+ "The architecture processes sequences in linear time making it efficient for long contexts. ",
645
+ "Knowledge is power and understanding is the key to wisdom. ",
646
+ "The future of artificial intelligence is bright and full of possibilities. ",
647
+ "Mathematics is the language in which the universe speaks to us. ",
648
+ "Innovation distinguishes between a leader and a follower. ",
649
+ "AICL is a structured programming language for building complex software systems. ",
650
+ "Cognitive memory enables the model to store and retrieve information across different time scales. ",
651
+ "The working memory holds recent information, episodic memory stores experiences, and semantic memory encodes general knowledge. ",
652
+ "Channel routing allows different aspects of the input to be processed by specialized pathways. ",
653
+ ]
654
+
655
+ fr_sentences = [
656
+ "Bonjour le monde est beau et la science est merveilleuse. ",
657
+ "Le futur de l'intelligence artificielle est prometteur et rempli de defis passionnants. ",
658
+ "CogNet est un modele de langage non-transformateur avec routage cognitif et memoire. ",
659
+ "La connaissance est le pouvoir et la comprehension est la cle de la sagesse. ",
660
+ "AICL est un langage de programmation structure pour construire des systemes logiciels complexes. ",
661
+ "L'apprentissage automatique transforme notre facon de resoudre les problemes. ",
662
+ ]
663
+
664
+ syn_ids = []
665
+ chars_gen = 0
666
+ rng = random.Random(42)
667
+
668
+ while chars_gen < target_synthetic_chars:
669
+ texts = []
670
+ for _ in range(400):
671
+ t = rng.choice(py_templates)
672
+ try:
673
+ text = t.format(
674
+ f=rng.choice(func_names), cls=rng.choice(cls_names),
675
+ p1=rng.choice(params), p2=rng.choice(params),
676
+ var=rng.choice(params[:5]), var2=rng.choice(params[:5]),
677
+ n=rng.randint(1, 100), func=rng.choice(func_names),
678
+ mod=rng.choice(['torch','numpy','json','os','sys','typing']),
679
+ mod2=rng.choice(['torch.nn','numpy','json','pathlib','collections']),
680
+ imp=rng.choice(['Model','Dataset','DataLoader','Config','Handler']),
681
+ expr=rng.choice(['x**2','x+1','x*2','len(x)','sum(data)']),
682
+ iter=rng.choice(['range(10)','data','items','values','batch']),
683
+ cond=rng.choice(['x > 0','x is not None','x % 2 == 0','len(x) > 0']),
684
+ stmt=rng.choice(['result = x * 2','continue','break','process(x)']),
685
+ )
686
+ texts.append(text)
687
+ except (KeyError, IndexError):
688
+ texts.append(f"x = {rng.randint(1,100)}\nresult = x * 2\n\n")
689
+ for _ in range(800):
690
+ texts.append(rng.choice(en_sentences))
691
+ for _ in range(400):
692
+ texts.append(rng.choice(fr_sentences))
693
+
694
+ batch_text = ''.join(texts)
695
+ batch_ids = tokenizer.encode(batch_text)
696
+ syn_ids.extend(batch_ids)
697
+ chars_gen += len(batch_text)
698
+
699
+ if len(syn_ids) % 50_000_000 < 10_000_000:
700
+ pct = chars_gen / target_synthetic_chars * 100
701
+ log.info(f' Synthetic: {chars_gen/1e6:.0f}M chars ({pct:.0f}%)')
702
+
703
+ syn_tensor = torch.tensor(syn_ids, dtype=torch.long)
704
+ torch.save(syn_tensor, syn_path)
705
+ log.info(f'Synthetic data saved: {syn_path} ({os.path.getsize(syn_path)/1e6:.0f} MB, {len(syn_ids):,} tokens)')
706
+ all_tensors.append(syn_tensor)
707
+ del syn_ids # Free memory
708
+
709
+ # ──────────────────────────────────────────────────────
710
+ # Part F: Merge ALL data + shuffle
711
+ # ──────────────────────────────────────────────────────
712
+ log.info('\n--- Part F: Merging ALL datasets ---')
713
+ log.info(f'Datasets to merge: {len(all_tensors)}')
714
+ for i, t in enumerate(all_tensors):
715
+ log.info(f' [{i}] {len(t):,} tokens')
716
+
717
+ merged = torch.cat(all_tensors, dim=0)
718
+ log.info(f'Total tokens before shuffle: {len(merged):,}')
719
+
720
+ # Shuffle to mix AICL + HF + scripts + synthetic
721
+ log.info(f'Shuffling {len(merged):,} tokens...')
722
+ perm = torch.randperm(len(merged))
723
+ merged = merged[perm]
724
+ del perm, all_tensors # Free memory
725
+
726
+ merged_path = os.path.join(DATA_DIR, 'train_merged.pt')
727
+ torch.save(merged, merged_path)
728
+ size_mb = os.path.getsize(merged_path) / 1e6
729
+ log.info(f'Merged dataset saved: {merged_path} ({size_mb:.0f} MB, {len(merged):,} tokens)')
730
+
731
+ del merged # Free memory
732
+
733
+ log.info(f'Data preparation complete. Files in {DATA_DIR}:')
734
+ for f in sorted(Path(DATA_DIR).glob('*.pt')):
735
+ log.info(f' {f.name} ({f.stat().st_size / 1e6:.0f} MB)')
736
+
737
+
738
+ # ═══════════════════════════════════════════════════════════
739
+ # Character Tokenizer (136 vocab)
740
+ # ═══════════════════════════════════════════════════════════
741
+ class CharTokenizer:
742
+ """Character-level tokenizer: 4 special + 132 printable/French chars = 136."""
743
+
744
+ def __init__(self, vocab_size=136):
745
+ self.vocab_size = vocab_size
746
+ self.pad_token_id = 0
747
+ self.unk_token_id = 1
748
+ self.bos_token_id = 2
749
+ self.eos_token_id = 3
750
+
751
+ chars = list(range(32, 127))
752
+ french = [192,193,194,195,196,197,199,200,201,202,203,204,205,206,207,
753
+ 210,211,212,213,214,217,218,219,220,224,225,226,227,228,229,
754
+ 231,232,233,234,235,236,237,238,239,242,243,244,245,246,249,
755
+ 250,251,252,253,255]
756
+ chars.extend(french)
757
+
758
+ self.char_to_id = {self.pad_token_id: 0, self.unk_token_id: 1,
759
+ self.bos_token_id: 2, self.eos_token_id: 3}
760
+ for i, c in enumerate(chars[:vocab_size - 4]):
761
+ self.char_to_id[c] = i + 4
762
+ self.id_to_char = {v: k for k, v in self.char_to_id.items()}
763
+
764
+ def encode(self, text):
765
+ ids = [self.bos_token_id]
766
+ for ch in text:
767
+ code = ord(ch)
768
+ ids.append(self.char_to_id.get(code, self.unk_token_id))
769
+ ids.append(self.eos_token_id)
770
+ return ids
771
+
772
+ def decode(self, ids):
773
+ chars = []
774
+ for i in ids:
775
+ if i in (self.pad_token_id, self.bos_token_id):
776
+ continue
777
+ if i == self.eos_token_id:
778
+ break
779
+ code = self.id_to_char.get(i, 0)
780
+ if code > 0:
781
+ chars.append(chr(code))
782
+ return ''.join(chars)
783
+
784
+ def save(self, path):
785
+ with open(path, 'w', encoding='utf-8') as f:
786
+ json.dump({
787
+ 'vocab_size': self.vocab_size,
788
+ 'char_to_id': {str(k): v for k, v in self.char_to_id.items()},
789
+ }, f, ensure_ascii=False, indent=2)
790
+ log.info(f'Tokenizer saved to {path}')
791
+
792
+ @classmethod
793
+ def load(cls, path):
794
+ with open(path, 'r', encoding='utf-8') as f:
795
+ data = json.load(f)
796
+ tok = cls.__new__(cls)
797
+ tok.vocab_size = data['vocab_size']
798
+ tok.char_to_id = {int(k): v for k, v in data['char_to_id'].items()}
799
+ tok.id_to_char = {v: k for k, v in tok.char_to_id.items()}
800
+ tok.pad_token_id = 0
801
+ tok.unk_token_id = 1
802
+ tok.bos_token_id = 2
803
+ tok.eos_token_id = 3
804
+ return tok
805
+
806
+
807
+ # ═══════════════════════════════════════════════════════════
808
+ # Dataset β€” supports multiple data formats
809
+ # ═══════════════════════════════════════════════════════════
810
+ class TokenDataset(Dataset):
811
+ def __init__(self, data_path, seq_len=512):
812
+ tokens = torch.load(data_path, map_location='cpu', weights_only=True)
813
+ if not isinstance(tokens, torch.LongTensor):
814
+ # Handle int16 or other dtypes from merge step
815
+ tokens = tokens.long()
816
+ self.tokens = tokens
817
+ self.seq_len = seq_len
818
+ log.info(f'Dataset: {len(self.tokens):,} tokens, {len(self):,} sequences')
819
+
820
+ def __len__(self):
821
+ return max(0, (len(self.tokens) - 1) // self.seq_len)
822
+
823
+ def __getitem__(self, idx):
824
+ start = idx * self.seq_len
825
+ end = start + self.seq_len + 1
826
+ chunk = self.tokens[start:end]
827
+ return chunk[:-1], chunk[1:]
828
+
829
+
830
+ class MultiFileDataset(Dataset):
831
+ """Load and concatenate multiple .pt token files."""
832
+ def __init__(self, data_dir, seq_len=512):
833
+ pt_files = sorted(Path(data_dir).glob('*.pt'))
834
+ if not pt_files:
835
+ raise FileNotFoundError(f'No .pt files found in {data_dir}')
836
+
837
+ log.info(f'Loading {len(pt_files)} data files...')
838
+ all_tokens = []
839
+ for f in pt_files:
840
+ log.info(f' Loading {f.name}...')
841
+ t = torch.load(f, map_location='cpu', weights_only=True)
842
+ all_tokens.append(t.long())
843
+
844
+ self.tokens = torch.cat(all_tokens, dim=0)
845
+ self.seq_len = seq_len
846
+ log.info(f'Combined dataset: {len(self.tokens):,} tokens, {len(self):,} sequences')
847
+
848
+ def __len__(self):
849
+ return max(0, (len(self.tokens) - 1) // self.seq_len)
850
+
851
+ def __getitem__(self, idx):
852
+ start = idx * self.seq_len
853
+ end = start + self.seq_len + 1
854
+ chunk = self.tokens[start:end]
855
+ return chunk[:-1], chunk[1:]
856
+
857
+
858
+ # ═══════════════════════════════════════════════════════════
859
+ # CogNet-1B Architecture (~1.06B params)
860
+ # ═══════════════════════════════════════════════════════════
861
+ class CognitiveMemory(nn.Module):
862
+ def __init__(self, dim, working_slots=128, episodic_slots=256, semantic_slots=512):
863
+ super().__init__()
864
+ self.working = nn.Parameter(torch.randn(working_slots, dim) * 0.02)
865
+ self.episodic = nn.Parameter(torch.randn(episodic_slots, dim) * 0.02)
866
+ self.semantic = nn.Parameter(torch.randn(semantic_slots, dim) * 0.02)
867
+ self.gate_w = nn.Linear(dim * 3, 3)
868
+ self.work_proj = nn.Linear(dim, dim)
869
+ self.epis_proj = nn.Linear(dim, dim)
870
+ self.seman_proj = nn.Linear(dim, dim)
871
+
872
+ def forward(self, x):
873
+ B, T, D = x.shape
874
+
875
+ w_att = torch.matmul(x, self.working.t())
876
+ w_att = F.softmax(w_att / math.sqrt(D), dim=-1)
877
+ w_out = self.work_proj(torch.matmul(w_att, self.working))
878
+
879
+ e_att = torch.matmul(x, self.episodic.t())
880
+ e_att = F.softmax(e_att / math.sqrt(D), dim=-1)
881
+ e_out = self.epis_proj(torch.matmul(e_att, self.episodic))
882
+
883
+ s_att = torch.matmul(x, self.semantic.t())
884
+ s_att = F.softmax(s_att / math.sqrt(D), dim=-1)
885
+ s_out = self.seman_proj(torch.matmul(s_att, self.semantic))
886
+
887
+ gate_in = torch.cat([w_out.mean(1), e_out.mean(1), s_out.mean(1)], dim=-1)
888
+ gates = F.softmax(self.gate_w(gate_in), dim=-1)
889
+
890
+ combined = (gates[:, 0:1, None] * w_out +
891
+ gates[:, 1:2, None] * e_out +
892
+ gates[:, 2:3, None] * s_out)
893
+ return x + combined
894
+
895
+
896
+ class CogNetBlock(nn.Module):
897
+ def __init__(self, dim, n_channels=8, channel_dim=384, ff_dim=8192,
898
+ working_slots=128, episodic_slots=256, semantic_slots=512):
899
+ super().__init__()
900
+ self.n_channels = n_channels
901
+
902
+ self.channel_projs = nn.ModuleList([nn.Linear(dim, channel_dim) for _ in range(n_channels)])
903
+ self.channel_merge = nn.Linear(n_channels * channel_dim, dim)
904
+ self.norm1 = nn.LayerNorm(dim)
905
+
906
+ self.ff = nn.Sequential(
907
+ nn.Linear(dim, ff_dim),
908
+ nn.GELU(),
909
+ nn.Linear(ff_dim, dim),
910
+ )
911
+ self.norm2 = nn.LayerNorm(dim)
912
+
913
+ self.cog_mem = CognitiveMemory(dim, working_slots, episodic_slots, semantic_slots)
914
+ self.norm3 = nn.LayerNorm(dim)
915
+
916
+ self.router = nn.Linear(dim, n_channels)
917
+
918
+ def forward(self, x):
919
+ B, T, D = x.shape
920
+
921
+ route_logits = self.router(x)
922
+ route_weights = F.softmax(route_logits, dim=-1)
923
+
924
+ channel_outs = []
925
+ for i, proj in enumerate(self.channel_projs):
926
+ ch_x = F.gelu(proj(x))
927
+ w = route_weights[:, :, i:i+1]
928
+ channel_outs.append(ch_x * w)
929
+
930
+ merged = self.channel_merge(torch.cat(channel_outs, dim=-1))
931
+ x = self.norm1(x + merged)
932
+
933
+ x = self.norm2(x + self.ff(x))
934
+ x = self.norm3(self.cog_mem(x))
935
+ return x
936
+
937
+
938
+ class CogNet1B(nn.Module):
939
+ def __init__(self, vocab_size=136, hidden_dim=2048, n_blocks=16,
940
+ n_channels=8, channel_dim=384, ff_dim=8192,
941
+ working_slots=128, episodic_slots=256, semantic_slots=512,
942
+ seq_len=512):
943
+ super().__init__()
944
+ self.vocab_size = vocab_size
945
+ self.hidden_dim = hidden_dim
946
+ self.seq_len = seq_len
947
+
948
+ self.token_emb = nn.Embedding(vocab_size, hidden_dim)
949
+ self.pos_emb = nn.Embedding(seq_len, hidden_dim)
950
+
951
+ self.blocks = nn.ModuleList([
952
+ CogNetBlock(hidden_dim, n_channels, channel_dim, ff_dim,
953
+ working_slots, episodic_slots, semantic_slots)
954
+ for _ in range(n_blocks)
955
+ ])
956
+
957
+ self.final_norm = nn.LayerNorm(hidden_dim)
958
+ self.head = nn.Linear(hidden_dim, vocab_size, bias=False)
959
+
960
+ # Weight tying
961
+ self.head.weight = self.token_emb.weight
962
+ self._init_weights()
963
+
964
+ def _init_weights(self):
965
+ for module in self.modules():
966
+ if isinstance(module, nn.Linear):
967
+ nn.init.normal_(module.weight, mean=0.0, std=0.02)
968
+ if module.bias is not None:
969
+ nn.init.zeros_(module.bias)
970
+ elif isinstance(module, nn.Embedding):
971
+ nn.init.normal_(module.weight, mean=0.0, std=0.02)
972
+
973
+ def forward(self, input_ids, targets=None):
974
+ B, T = input_ids.shape
975
+ pos = torch.arange(T, device=input_ids.device).unsqueeze(0)
976
+ x = self.token_emb(input_ids) + self.pos_emb(pos)
977
+
978
+ for block in self.blocks:
979
+ x = block(x)
980
+
981
+ x = self.final_norm(x)
982
+ logits = self.head(x)
983
+
984
+ loss = None
985
+ if targets is not None:
986
+ loss = F.cross_entropy(
987
+ logits.view(-1, self.vocab_size),
988
+ targets.view(-1),
989
+ ignore_index=0,
990
+ )
991
+ return logits, loss
992
+
993
+ @torch.no_grad()
994
+ def generate(self, input_ids, max_new_tokens=200, temperature=0.8, top_k=50):
995
+ self.eval()
996
+ for _ in range(max_new_tokens):
997
+ idx_cond = input_ids[:, -self.seq_len:]
998
+ logits, _ = self(idx_cond)
999
+ logits = logits[:, -1, :] / temperature
1000
+ if top_k > 0:
1001
+ v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
1002
+ logits[logits < v[:, [-1]]] = float('-inf')
1003
+ probs = F.softmax(logits, dim=-1)
1004
+ next_id = torch.multinomial(probs, num_samples=1)
1005
+ input_ids = torch.cat([input_ids, next_id], dim=1)
1006
+ return input_ids
1007
+
1008
+
1009
+ # ═══════════════════════════════════════════════════════════
1010
+ # Learning Rate Schedule
1011
+ # ═══════════════════════════════════════════════════════════
1012
+ def get_cosine_lr(step, warmup_steps, max_steps, max_lr, min_lr):
1013
+ if step < warmup_steps:
1014
+ return max_lr * step / max(1, warmup_steps)
1015
+ if step >= max_steps:
1016
+ return min_lr
1017
+ progress = (step - warmup_steps) / max(1, max_steps - warmup_steps)
1018
+ return min_lr + 0.5 * (max_lr - min_lr) * (1 + math.cos(math.pi * progress))
1019
+
1020
+
1021
+ # ═══════════════════════════════════════════════════════════
1022
+ # Save checkpoint
1023
+ # ═══════════════════════════════════════════════════════════
1024
+ def save_checkpoint(model, optimizer, step, loss_val, best_loss, tokenizer, args, filename):
1025
+ path = os.path.join(args.ckpt_dir, filename)
1026
+ save_dict = {
1027
+ 'step': step,
1028
+ 'model_state_dict': model.state_dict(),
1029
+ 'optimizer_state_dict': optimizer.state_dict(),
1030
+ 'loss': loss_val,
1031
+ 'best_loss': best_loss,
1032
+ 'config': {
1033
+ 'vocab_size': tokenizer.vocab_size,
1034
+ 'hidden_dim': 2048,
1035
+ 'n_blocks': 16,
1036
+ 'n_channels': 8,
1037
+ 'channel_dim': 384,
1038
+ 'ff_dim': 8192,
1039
+ 'working_slots': 128,
1040
+ 'episodic_slots': 256,
1041
+ 'semantic_slots': 512,
1042
+ 'seq_len': args.seq_len,
1043
+ },
1044
+ }
1045
+ torch.save(save_dict, path)
1046
+ return path
1047
+
1048
+
1049
+ # ═══════════════════════════════════════════════════════════
1050
+ # HuggingFace Push
1051
+ # ═══════════════════════════════════════════════════════════
1052
+ def push_to_huggingface(ckpt_path, tokenizer):
1053
+ """Push the best checkpoint to HuggingFace."""
1054
+ try:
1055
+ from huggingface_hub import HfApi
1056
+ repo_id = os.environ.get('HF_REPO', HF_REPO)
1057
+
1058
+ api = HfApi()
1059
+ api.create_repo(repo_id=repo_id, exist_ok=True, token=HF_TOKEN)
1060
+
1061
+ # Upload checkpoint
1062
+ api.upload_file(path_or_fileobj=ckpt_path, path_in_repo='checkpoints/cognet_1b_best.pt',
1063
+ repo_id=repo_id, token=HF_TOKEN)
1064
+ # Upload tokenizer
1065
+ api.upload_file(path_or_fileobj=TOKENIZER_PATH, path_in_repo='checkpoints/tokenizer_v3.json',
1066
+ repo_id=repo_id, token=HF_TOKEN)
1067
+
1068
+ log.info(f'Pushed to HuggingFace: {repo_id}')
1069
+ except Exception as e:
1070
+ log.warning(f'HF push failed (non-fatal): {e}')
1071
+
1072
+
1073
+ # ═══════════════════════════════════════════════════════════
1074
+ # Find training data
1075
+ # ═══════════════════════════════════════════════════════════
1076
+ def find_data_path(args):
1077
+ """Find the best training data file/directory."""
1078
+ # 1. Explicit path
1079
+ if args.data_path and os.path.exists(args.data_path):
1080
+ return args.data_path
1081
+
1082
+ # 2. train_merged.pt (from cognet_data_prep.py merge step)
1083
+ merged = os.path.join(DATA_DIR, 'train_merged.pt')
1084
+ if os.path.exists(merged):
1085
+ return merged
1086
+
1087
+ # 3. aicl_10x.pt (from fallback generation)
1088
+ aicl = os.path.join(DATA_DIR, 'aicl_10x.pt')
1089
+ if os.path.exists(aicl):
1090
+ return aicl
1091
+
1092
+ # 4. Multiple .pt files β€” use data directory
1093
+ pt_files = list(Path(DATA_DIR).glob('*.pt'))
1094
+ if len(pt_files) >= 2:
1095
+ return DATA_DIR # Use MultiFileDataset
1096
+
1097
+ # 5. Single .pt file
1098
+ if pt_files:
1099
+ return str(pt_files[0])
1100
+
1101
+ return None
1102
+
1103
+
1104
+ # ═══════════════════════════════════════════════════════════
1105
+ # Main Training Loop
1106
+ # ═══════════════════════════════════════════════════════════
1107
+ def main():
1108
+ parser = argparse.ArgumentParser()
1109
+ parser.add_argument('--batch-size', type=int, default=4)
1110
+ parser.add_argument('--grad-accum', type=int, default=8,
1111
+ help='Gradient accumulation steps (effective batch = batch-size * grad-accum)')
1112
+ parser.add_argument('--seq-len', type=int, default=512)
1113
+ parser.add_argument('--max-steps', type=int, default=100000)
1114
+ parser.add_argument('--warmup-steps', type=int, default=2000)
1115
+ parser.add_argument('--max-lr', type=float, default=1e-4)
1116
+ parser.add_argument('--min-lr', type=float, default=1e-5)
1117
+ parser.add_argument('--weight-decay', type=float, default=0.1)
1118
+ parser.add_argument('--grad-clip', type=float, default=1.0)
1119
+ parser.add_argument('--save-every', type=int, default=2000)
1120
+ parser.add_argument('--eval-every', type=int, default=500)
1121
+ parser.add_argument('--log-every', type=int, default=50)
1122
+ parser.add_argument('--data-path', type=str, default=None,
1123
+ help='Explicit path to training data .pt file or directory')
1124
+ parser.add_argument('--ckpt-dir', type=str, default=CKPT_DIR)
1125
+ parser.add_argument('--resume', type=str, default=None)
1126
+ parser.add_argument('--bf16', action='store_true', default=True)
1127
+ parser.add_argument('--skip-data-prep', action='store_true',
1128
+ help='Skip data preparation (data already exists)')
1129
+ parser.add_argument('--compile', action='store_true', default=False,
1130
+ help='Use torch.compile on model')
1131
+ args = parser.parse_args()
1132
+
1133
+ # ── Setup ──
1134
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
1135
+ log.info('='*60)
1136
+ log.info('CogNet-1B Training β€” RunPod AICL Edition (RTX PRO 6000)')
1137
+ log.info('='*60)
1138
+ log.info(f'Device: {device}')
1139
+ log.info(f'Workspace: {WORKSPACE}')
1140
+ log.info(f'Checkpoint dir: {args.ckpt_dir}')
1141
+ log.info(f'HuggingFace repo: {HF_REPO}')
1142
+
1143
+ if torch.cuda.is_available():
1144
+ for i in range(torch.cuda.device_count()):
1145
+ props = torch.cuda.get_device_properties(i)
1146
+ log.info(f'GPU {i}: {props.name} β€” {props.total_memory/1e9:.1f} GB VRAM')
1147
+
1148
+ # ── Step 1: Prepare data (AICL conversion + synthetic) ──
1149
+ prepare_data(skip=args.skip_data_prep)
1150
+
1151
+ # ── Step 2: Load tokenizer ──
1152
+ tokenizer_paths = [
1153
+ TOKENIZER_PATH,
1154
+ os.path.join(DATA_DIR, 'tokenizer_v3.json'),
1155
+ os.path.join(WORKSPACE, 'hf_scripts', 'tokenizer_v3.json'),
1156
+ ]
1157
+
1158
+ tokenizer = None
1159
+ for tp in tokenizer_paths:
1160
+ if os.path.exists(tp):
1161
+ tokenizer = CharTokenizer.load(tp)
1162
+ log.info(f'Loaded tokenizer from {tp} (vocab={tokenizer.vocab_size})')
1163
+ # Copy to expected location
1164
+ if tp != TOKENIZER_PATH:
1165
+ import shutil
1166
+ shutil.copy2(tp, TOKENIZER_PATH)
1167
+ log.info(f'Copied tokenizer to {TOKENIZER_PATH}')
1168
+ break
1169
+
1170
+ if tokenizer is None:
1171
+ tokenizer = CharTokenizer()
1172
+ tokenizer.save(TOKENIZER_PATH)
1173
+ log.info(f'Created tokenizer (vocab={tokenizer.vocab_size})')
1174
+
1175
+ # ── Step 3: Load dataset ──
1176
+ data_path = find_data_path(args)
1177
+ if data_path is None:
1178
+ log.error('No training data found! Run without --skip-data-prep or provide --data-path')
1179
+ sys.exit(1)
1180
+
1181
+ log.info(f'Loading training data from: {data_path}')
1182
+
1183
+ if os.path.isdir(data_path):
1184
+ dataset = MultiFileDataset(data_path, args.seq_len)
1185
+ else:
1186
+ dataset = TokenDataset(data_path, args.seq_len)
1187
+
1188
+ dataloader = DataLoader(dataset, batch_size=args.batch_size, shuffle=True,
1189
+ num_workers=4, pin_memory=True, drop_last=True)
1190
+
1191
+ # ── Step 4: Build model ──
1192
+ log.info('Building CogNet-1B (~1.06B params)...')
1193
+ model = CogNet1B(
1194
+ vocab_size=tokenizer.vocab_size,
1195
+ hidden_dim=2048, n_blocks=16, n_channels=8,
1196
+ channel_dim=384, ff_dim=8192,
1197
+ working_slots=128, episodic_slots=256, semantic_slots=512,
1198
+ seq_len=args.seq_len,
1199
+ ).to(device)
1200
+
1201
+ total_params = sum(p.numel() for p in model.parameters())
1202
+ log.info(f'Total parameters: {total_params:,} ({total_params/1e9:.2f}B)')
1203
+
1204
+ # Optional: torch.compile for speed
1205
+ if args.compile:
1206
+ log.info('Compiling model with torch.compile...')
1207
+ model = torch.compile(model)
1208
+
1209
+ # ── Step 5: Optimizer ──
1210
+ optimizer = torch.optim.AdamW(
1211
+ model.parameters(), lr=args.max_lr,
1212
+ betas=(0.9, 0.95), eps=1e-8,
1213
+ weight_decay=args.weight_decay, fused=True,
1214
+ )
1215
+
1216
+ use_bf16 = args.bf16 and torch.cuda.is_bf16_supported()
1217
+ scaler = None if use_bf16 else torch.amp.GradScaler('cuda')
1218
+ log.info(f'Mixed precision: {"BF16" if use_bf16 else "FP16+GradScaler"}')
1219
+
1220
+ # ── Resume ──
1221
+ start_step = 0
1222
+ best_loss = float('inf')
1223
+
1224
+ # Try explicit resume path first
1225
+ if args.resume and os.path.exists(args.resume):
1226
+ log.info(f'Resuming from {args.resume}...')
1227
+ ckpt = torch.load(args.resume, map_location=device, weights_only=False)
1228
+ model.load_state_dict(ckpt['model_state_dict'])
1229
+ if 'optimizer_state_dict' in ckpt:
1230
+ optimizer.load_state_dict(ckpt['optimizer_state_dict'])
1231
+ start_step = ckpt.get('step', 0)
1232
+ best_loss = ckpt.get('best_loss', float('inf'))
1233
+ log.info(f'Resumed at step {start_step}, best_loss={best_loss:.4f}')
1234
+
1235
+ # Try auto-resume from latest checkpoint
1236
+ if start_step == 0:
1237
+ latest = os.path.join(args.ckpt_dir, 'cognet_1b_latest.pt')
1238
+ if os.path.exists(latest):
1239
+ log.info(f'Auto-resuming from {latest}...')
1240
+ ckpt = torch.load(latest, map_location=device, weights_only=False)
1241
+ model.load_state_dict(ckpt['model_state_dict'])
1242
+ if 'optimizer_state_dict' in ckpt:
1243
+ optimizer.load_state_dict(ckpt['optimizer_state_dict'])
1244
+ start_step = ckpt.get('step', 0)
1245
+ best_loss = ckpt.get('best_loss', float('inf'))
1246
+ log.info(f'Resumed at step {start_step}, best_loss={best_loss:.4f}')
1247
+
1248
+ # Also check for any step checkpoints newer than latest
1249
+ if start_step == 0:
1250
+ step_ckpts = sorted(Path(args.ckpt_dir).glob('cognet_1b_step_*.pt'))
1251
+ if step_ckpts:
1252
+ # Find the one with the highest step number
1253
+ latest_step_ckpt = max(
1254
+ step_ckpts,
1255
+ key=lambda p: int(p.stem.split('_')[-1])
1256
+ )
1257
+ log.info(f'Found step checkpoint: {latest_step_ckpt}')
1258
+ ckpt = torch.load(str(latest_step_ckpt), map_location=device, weights_only=False)
1259
+ model.load_state_dict(ckpt['model_state_dict'])
1260
+ if 'optimizer_state_dict' in ckpt:
1261
+ optimizer.load_state_dict(ckpt['optimizer_state_dict'])
1262
+ start_step = ckpt.get('step', 0)
1263
+ best_loss = ckpt.get('best_loss', float('inf'))
1264
+ log.info(f'Resumed at step {start_step}, best_loss={best_loss:.4f}')
1265
+
1266
+ # ── Train ──
1267
+ effective_batch = args.batch_size * args.grad_accum
1268
+ log.info(f'Starting: step {start_step} -> {args.max_steps}')
1269
+ log.info(f'Batch={args.batch_size} x GradAccum={args.grad_accum} = Effective {effective_batch}')
1270
+ log.info(f'SeqLen={args.seq_len}, LR={args.min_lr}-{args.max_lr}')
1271
+ log.info(f'Checkpoints: {args.ckpt_dir} (NETWORK VOLUME)')
1272
+ log.info(f'Graceful shutdown: SIGTERM/SIGINT will save checkpoint')
1273
+ log.info(f'Data: {data_path}')
1274
+
1275
+ model.train()
1276
+ data_iter = iter(dataloader)
1277
+ t0 = time.time()
1278
+ loss_val = 0.0
1279
+ accum_loss = 0.0
1280
+
1281
+ for step in range(start_step, args.max_steps):
1282
+ if shutdown_requested:
1283
+ log.info('Shutdown requested, saving checkpoint...')
1284
+ save_checkpoint(model, optimizer, step, loss_val, best_loss, tokenizer, args,
1285
+ 'cognet_1b_latest.pt')
1286
+ log.info(f'Checkpoint saved at step {step}. Exiting.')
1287
+ break
1288
+
1289
+ lr = get_cosine_lr(step, args.warmup_steps, args.max_steps, args.max_lr, args.min_lr)
1290
+ for param_group in optimizer.param_groups:
1291
+ param_group['lr'] = lr
1292
+
1293
+ # ── Gradient accumulation ──
1294
+ optimizer.zero_grad(set_to_none=True)
1295
+ accum_loss = 0.0
1296
+
1297
+ for micro_step in range(args.grad_accum):
1298
+ try:
1299
+ batch = next(data_iter)
1300
+ except StopIteration:
1301
+ data_iter = iter(dataloader)
1302
+ batch = next(data_iter)
1303
+
1304
+ x, y = batch
1305
+ x = x.to(device, non_blocking=True)
1306
+ y = y.to(device, non_blocking=True)
1307
+
1308
+ if use_bf16:
1309
+ with torch.amp.autocast('cuda', dtype=torch.bfloat16):
1310
+ logits, loss = model(x, y)
1311
+ (loss / args.grad_accum).backward()
1312
+ else:
1313
+ with torch.amp.autocast('cuda', dtype=torch.float16):
1314
+ logits, loss = model(x, y)
1315
+ scaler.scale(loss / args.grad_accum).backward()
1316
+
1317
+ accum_loss += loss.item()
1318
+
1319
+ # Step optimizer after accumulation
1320
+ if use_bf16:
1321
+ grad_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
1322
+ optimizer.step()
1323
+ else:
1324
+ scaler.unscale_(optimizer)
1325
+ grad_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
1326
+ scaler.step(optimizer)
1327
+ scaler.update()
1328
+
1329
+ loss_val = accum_loss / args.grad_accum
1330
+
1331
+ # Logging
1332
+ if step % args.log_every == 0:
1333
+ elapsed = time.time() - t0
1334
+ steps_per_sec = args.log_every / max(elapsed, 0.001)
1335
+ tokens_per_sec = steps_per_sec * effective_batch * args.seq_len
1336
+ vram_used = torch.cuda.memory_allocated() / 1e9
1337
+
1338
+ log.info(
1339
+ f'Step {step:>7d}/{args.max_steps} | '
1340
+ f'Loss: {loss_val:.4f} | PPL: {math.exp(min(loss_val, 20)):.1f} | '
1341
+ f'LR: {lr:.2e} | Grad: {grad_norm:.2f} | '
1342
+ f'VRAM: {vram_used:.1f}GB | {tokens_per_sec:.0f} tok/s'
1343
+ )
1344
+ t0 = time.time()
1345
+
1346
+ # Sample generation
1347
+ if step > 0 and step % args.eval_every == 0:
1348
+ model.eval()
1349
+ with torch.no_grad():
1350
+ prompt = torch.tensor([[tokenizer.bos_token_id]], device=device)
1351
+ sample_ids = model.generate(prompt, max_new_tokens=150, temperature=0.8, top_k=50)
1352
+ sample_text = tokenizer.decode(sample_ids[0].tolist())
1353
+ log.info(f'--- Sample step {step} ---')
1354
+ log.info(sample_text[:300])
1355
+ log.info(f'--- End ---')
1356
+ model.train()
1357
+
1358
+ # Save checkpoint
1359
+ if step > 0 and step % args.save_every == 0:
1360
+ save_checkpoint(model, optimizer, step, loss_val, best_loss, tokenizer, args,
1361
+ f'cognet_1b_step_{step}.pt')
1362
+ save_checkpoint(model, optimizer, step, loss_val, best_loss, tokenizer, args,
1363
+ 'cognet_1b_latest.pt')
1364
+
1365
+ if loss_val < best_loss:
1366
+ best_loss = loss_val
1367
+ save_checkpoint(model, optimizer, step, loss_val, best_loss, tokenizer, args,
1368
+ 'cognet_1b_best.pt')
1369
+ log.info(f'New best! loss={loss_val:.4f}')
1370
+
1371
+ # Clean up old step checkpoints (keep only last 3)
1372
+ step_ckpts = sorted(Path(args.ckpt_dir).glob('cognet_1b_step_*.pt'))
1373
+ if len(step_ckpts) > 3:
1374
+ for old_ckpt in step_ckpts[:-3]:
1375
+ old_ckpt.unlink()
1376
+ log.info(f'Removed old checkpoint: {old_ckpt.name}')
1377
+
1378
+ log.info(f'Checkpoint saved at step {step} (loss={loss_val:.4f}, best={best_loss:.4f})')
1379
+
1380
+ else:
1381
+ # Training completed normally (not broken by shutdown)
1382
+ final_path = save_checkpoint(model, optimizer, args.max_steps, loss_val, best_loss,
1383
+ tokenizer, args, 'cognet_1b_final.pt')
1384
+ log.info(f'Training complete! Final loss: {loss_val:.4f}, Best: {best_loss:.4f}')
1385
+
1386
+ # Push to HuggingFace
1387
+ best_path = os.path.join(args.ckpt_dir, 'cognet_1b_best.pt')
1388
+ if os.path.exists(best_path):
1389
+ push_to_huggingface(best_path, tokenizer)
1390
+
1391
+
1392
+ if __name__ == '__main__':
1393
+ main()