File size: 5,645 Bytes
f57cdb9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python
"""PALIMPSESTE β€” Train the 1B-scale model on real data.

Combines the built-in conversational corpus with a real trivia dataset
(TriviaQA, 3000 Q/A pairs) and trains at D=100,000 (the '1b' preset β€”
theoretical capacity ~2^25000 β‰ˆ 10^7525 associations).

Usage:
  python examples/train_1b.py --output ./palimpseste-1b --n-trivia 2000
  python examples/train_1b.py --output ./palimpseste-1b --push-to-hub thefinalboss/palimpseste-1b
"""

from __future__ import annotations

import argparse
import json
import sys
import time
from pathlib import Path

import numpy as np

sys.path.insert(0, str(Path(__file__).resolve().parent))
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from palimseste.lm import PalimpsesteConfig, PRESETS
from palimseste.hf import HFPalimpsesteLM
from corpus_chat import get_corpus as get_chat_corpus


def parse_args() -> argparse.Namespace:
    p = argparse.ArgumentParser(
        description="Train the PALIMPSESTE 1B-scale model on real data.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    p.add_argument("--output", "-o", type=str, required=True)
    p.add_argument("--n-trivia", type=int, default=2000,
                   help="number of TriviaQA pairs to use (default 2000)")
    p.add_argument("--trivia-file", type=str, default="trivia_qa_pairs.json",
                   help="path to extracted TriviaQA pairs JSON")
    p.add_argument("--D", type=int, default=100000,
                   help="hypervector dimension (default 100000 = 1b preset)")
    p.add_argument("--context-window", type=int, default=80)
    p.add_argument("--kernel-radius", type=int, default=800)
    p.add_argument("--push-to-hub", type=str, default=None)
    p.add_argument("--seed", type=int, default=42)
    return p.parse_args()


def load_trivia(path: str, n: int) -> list[tuple[str, str]]:
    p = Path(path)
    if not p.exists():
        print(f"warning: {path} not found, downloading TriviaQA...", file=sys.stderr)
        from datasets import load_dataset
        ds = load_dataset('mandarjoshi/trivia_qa', 'rc.nocontext',
                          split='train', streaming=True)
        pairs = []
        for i, ex in enumerate(ds):
            q = ex['question'].strip().lower()
            ans = ex['answer']['value'].strip().lower()
            if q and ans and len(ans) < 100:
                pairs.append((q, ans))
            if i >= n * 2:
                break
        with open(path, 'w', encoding='utf-8') as f:
            json.dump([{'q': q, 'a': a} for q, a in pairs], f, ensure_ascii=False)
        return pairs[:n]
    with open(path, 'r', encoding='utf-8') as f:
        data = json.load(f)
    return [(d['q'], d['a']) for d in data[:n]]


def main() -> None:
    args = parse_args()

    # load data
    chat_pairs = get_chat_corpus()
    trivia_pairs = load_trivia(args.trivia_file, args.n_trivia)
    all_pairs = chat_pairs + trivia_pairs

    print("=" * 70)
    print("PALIMPSESTE β€” 1B-SCALE TRAINING ON REAL DATA")
    print("=" * 70)
    cap_log2 = float(np.log2(0.14 * args.D) + args.D / 4.0)
    print(f"D = {args.D:,}  (theoretical capacity: 2^{cap_log2:.0f} β‰ˆ 10^{cap_log2*0.30103:.0f})")
    print(f"context_window = {args.context_window}  kernel_radius = {args.kernel_radius}")
    print(f"chat corpus: {len(chat_pairs)} pairs")
    print(f"trivia corpus: {len(trivia_pairs)} pairs")
    print(f"total: {len(all_pairs)} pairs")
    print()

    config = PalimpsesteConfig(
        D=args.D,
        context_window=args.context_window,
        kernel_radius=args.kernel_radius,
        temperature=0.3,
    )
    rng = np.random.default_rng(args.seed)
    lm = HFPalimpsesteLM(config=config, rng=rng)

    # build vocab from all text
    full_text = "".join(q + a for q, a in all_pairs)
    lm.build_tokenizer(full_text)
    print(f"vocab: {lm.tokenizer.vocab_size} chars")

    # train
    t0 = time.perf_counter()
    n_tokens = lm.train_on_qa_pairs(all_pairs, verbose=True)
    dt = time.perf_counter() - t0
    print(f"\ntrained: {n_tokens:,} tokens in {dt:.1f}s ({n_tokens/dt:.0f} tok/s)")
    print(f"|M| = {len(lm.mem):,} traces")

    # test on chat questions + a few trivia
    print("\n--- chat quality test ---")
    chat_tests = ["bonjour", "qui es-tu", "tu utilises un gpu", "who are you",
                   "what is python", "merci"]
    for q in chat_tests:
        r = lm.respond(q, temperature=0.0, seed=0)
        ok = "βœ“" if r and "desole" not in r.lower() else "βœ—"
        print(f"  {ok} {q:30s} -> {r[:70]}")

    print("\n--- trivia recall test (exact match required) ---")
    trivia_tests = trivia_pairs[:10]
    correct = 0
    for q, expected in trivia_tests:
        r = lm.respond(q, temperature=0.0, seed=0)
        ok = "βœ“" if expected in r else "βœ—"
        if expected in r:
            correct += 1
        print(f"  {ok} {q[:50]:50s} -> {r[:40]} (expected: {expected})")
    print(f"  trivia recall: {correct}/{len(trivia_tests)}")

    # save
    out = Path(args.output)
    print(f"\nsaving to {out} ...")
    lm.save_pretrained(out)
    sz = sum(f.stat().st_size for f in out.rglob('*') if f.is_file())
    print(f"saved: {sz / 1e6:.1f} MB")

    # push
    if args.push_to_hub:
        print(f"\npushing to HF Hub: {args.push_to_hub} ...")
        url = lm.push_to_hub(args.push_to_hub)
        print(f"pushed: {url}")

    print("\n" + "=" * 70)
    print("DONE. Load with:")
    print(f"  from palimseste.hf import HFPalimpsesteLM")
    print(f"  lm = HFPalimpsesteLM.from_pretrained('{args.push_to_hub or out}')")
    print("=" * 70)


if __name__ == "__main__":
    main()