#!/usr/bin/env python """PALIMPSESTE — Train a causal LM and save it (HF-compatible format). Usage: # train on a text file with the 'small' preset (D=10000) python examples/train_lm.py --input corpus.txt --preset small --output ./my_model # train on inline text python examples/train_lm.py --text "your training text here" --preset tiny --output ./my_model # train at 1B-scale capacity (D=100000) — large memory, slow but no GPU python examples/train_lm.py --input corpus.txt --preset 1b --output ./palimpseste-1b # train and push to Hugging Face Hub python examples/train_lm.py --input corpus.txt --preset small --output ./my_model --push-to-hub user/palimpseste-small # custom config python examples/train_lm.py --input corpus.txt --output ./my_model --D 20000 --context-window 48 --kernel-radius 300 Training is O(1) per token — no epochs, no gradient, no GPU. The "model" is an append-only memory of (context_hv -> token_hv) associations. The millionth token costs exactly as much as the first. """ from __future__ import annotations import argparse import sys import time from pathlib import Path import numpy as np # allow running from the repo root without installing sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from palimseste.lm import PalimpsesteConfig, PRESETS from palimseste.hf import HFPalimpsesteLM def parse_args() -> argparse.Namespace: p = argparse.ArgumentParser( description="Train a PALIMPSESTE causal LM (no GPU, no gradient).", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__, ) g = p.add_mutually_exclusive_group(required=True) g.add_argument("--input", type=str, help="path to a UTF-8 text file to train on") g.add_argument("--text", type=str, help="inline training text") p.add_argument("--output", "-o", type=str, required=True, help="output directory for the saved model") p.add_argument("--preset", type=str, default=None, choices=list(PRESETS.keys()), help="use a capacity preset (tiny/small/medium/large/1b)") p.add_argument("--D", type=int, default=None, help="hypervector dimensionality") p.add_argument("--context-window", type=int, default=None) p.add_argument("--kernel-radius", type=int, default=None) p.add_argument("--temperature", type=float, default=0.5) p.add_argument("--push-to-hub", type=str, default=None, help="push to HF Hub repo (e.g. user/palimpseste-small)") p.add_argument("--hub-token", type=str, default=None) p.add_argument("--seed", type=int, default=42) return p.parse_args() def build_config(args: argparse.Namespace) -> PalimpsesteConfig: if args.preset: cfg = PRESETS[args.preset] # allow overrides if args.D is not None: cfg = PalimpsesteConfig(D=args.D, context_window=cfg.context_window, kernel_radius=cfg.kernel_radius) if args.context_window is not None: cfg.context_window = args.context_window if args.kernel_radius is not None: cfg.kernel_radius = args.kernel_radius cfg.temperature = args.temperature return cfg # fully custom D = args.D or 10_000 return PalimpsesteConfig( D=D, context_window=args.context_window or 32, kernel_radius=args.kernel_radius or 200, temperature=args.temperature, ) def load_text(args: argparse.Namespace) -> str: if args.text is not None: return args.text p = Path(args.input) if not p.exists(): sys.exit(f"error: input file not found: {p}") return p.read_text(encoding="utf-8") def main() -> None: args = parse_args() config = build_config(args) text = load_text(args) print("=" * 70) print("PALIMPSESTE — causal LM training") print("=" * 70) print(f"config: D={config.D:,} context_window={config.context_window} " f"kernel_radius={config.kernel_radius} temperature={config.temperature}") cap_log2 = float(np.log2(0.14 * config.D) + config.D / 4.0) print(f"theoretical capacity: 2^{cap_log2:.1f} " f"≈ 10^{cap_log2 * 0.30103:.1f} associations") print(f"corpus: {len(text):,} chars") print() rng = np.random.default_rng(args.seed) lm = HFPalimpsesteLM(config=config, rng=rng) # build tokenizer t0 = time.perf_counter() lm.build_tokenizer(text) print(f"vocab built: {lm.tokenizer.vocab_size} chars " f"({time.perf_counter() - t0:.2f}s)") # train (O(1) per token) t0 = time.perf_counter() n_tokens = lm.train_on_text(text, verbose=True) dt = time.perf_counter() - t0 print(f"\ntrained: {n_tokens:,} tokens in {dt:.2f}s " f"({n_tokens / max(dt, 1e-9):,.0f} tok/s)") print(f"|M| = {len(lm.mem):,} traces") # quick self-eval ev = lm.evaluate(text) print(f"\nself-eval: next_token_accuracy={ev['next_token_accuracy']:.3f} " f"mean_surprise={ev['mean_surprise']:.3f}") # sample generation sample_prompts = ["", "the ", "a "] print("\n--- sample generation ---") for prompt in sample_prompts: out = lm.generate(prompt, max_new_tokens=40, temperature=0.5, seed=args.seed) print(f" {prompt!r:8s} -> {out.text!r}") # save out_dir = Path(args.output) print(f"\nsaving to {out_dir} ...") lm.save_pretrained(out_dir) print(f"saved. files: {sorted(p.name for p in out_dir.iterdir())}") # push to hub if args.push_to_hub: print(f"\npushing to HF Hub: {args.push_to_hub} ...") url = lm.push_to_hub(args.push_to_hub, token=args.hub_token) print(f"pushed: {url}") print("\n" + "=" * 70) print("done. Load with:") print(f" from palimseste.hf import HFPalimpsesteLM") print(f" lm = HFPalimpsesteLM.from_pretrained('{out_dir}')") print("=" * 70) if __name__ == "__main__": main()