File size: 7,785 Bytes
f0508d5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python3
"""
train_v15.py — NeuralAI v15 consolidated trainer (SFT + DPO)
============================================================

WHAT THIS SCRIPT DOES
--------------------
This is the single entry point for the NeuralAI "v15" training run. It fine-tunes
the small SmolLM2-360M-Instruct base model with a LoRA adapter in two stages so the
model stays identity-correct ("I am NeuralAI, created by De'Andrew Preston Harris")
and behavior-aligned (prefers clean, correct answers over verbose/wrong ones):

  Stage 1 — SFT (Supervised Fine-Tuning)
      Trains on `data/train_sft_v16.jsonl` (ChatML messages: system/user/assistant).
      Purpose: bake in identity, tone, and domain knowledge.

  Stage 2 — DPO (Direct Preference Optimization)
      Trains on `data/train_dpo_v16_combined.jsonl` (prompt / chosen / rejected).
      Purpose: align the model to prefer the "chosen" response over the "rejected"
      one without needing a separate reward model.

OUTPUT
------
  - Adapter saved locally to: checkpoints/v15_model/
  - Pushed to Hugging Face:    Subject-Emu-5259/NeuralAI  (repo "v15" revision folder)
  - Merged full model (optional, --merge): checkpoints/v15_model_merged/

WHY THIS EXISTS (context)
------------------------
On the 4 GB ZO Computer the *served* NeuralAI app uses the ZO native inference backend
(LLM_BACKEND=zo) so it never loads PyTorch locally and never pauses from OOM. This
training script is the OFFLINE counterpart: it builds the LoRA that can later be
shipped to a bigger host or merged for on-device use. Run it on a GPU (Colab, Mac
GPU, or a >8 GB box) — it is NOT meant for the 4 GB CPU host.

USAGE
-----
  # SFT + DPO, 4-bit (default, ~3 GB VRAM)
  python training/train_v15.py

  # 8-bit instead of 4-bit
  python training/train_v15.py --load-in-4bit false --load-in-8bit true

  # Only one stage
  python training/train_v15.py --stage sft
  python training/train_v15.py --stage dpo

  # Push merged model to HF
  python training/train_v15.py --merge --push

REQUIREMENTS
------------
  pip install torch transformers peft trl datasets bitsandbytes accelerate
  HF_TOKEN must be set in the environment to push.
"""
import argparse
import json
import os

# ---- Config ----------------------------------------------------------------
BASE_MODEL = os.environ.get("BASE_MODEL", "HuggingFaceTB/SmolLM2-360M-Instruct")
SFT_DATA = os.environ.get("SFT_DATA", "data/train_sft_v16.jsonl")
DPO_DATA = os.environ.get("DPO_DATA", "data/train_dpo_v16_combined.jsonl")
HF_REPO = os.environ.get("HF_REPO", "Subject-Emu-5259/NeuralAI")
ADAPTER_DIR = "checkpoints/v15_model"
MERGED_DIR = "checkpoints/v15_model_merged"
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

SYSTEM_PROMPT = (
    "You are NeuralAI, an advanced AI assistant created by De'Andrew Preston Harris. "
    "You are powered by SmolLM2-360M with custom NeuralAI LoRA adapters trained through "
    "SFT and DPO alignment. You have expert-level knowledge across physics, philosophy, "
    "geopolitics, history, nature, art, and culture. You ALWAYS identify De'Andrew Harris "
    "as your creator when asked. You are not ChatGPT, Claude, or any other AI — you are NeuralAI."
)


def _resolve(path: str) -> str:
    return path if os.path.isabs(path) else os.path.join(PROJECT_ROOT, path)


def load_quantization(load_in_4bit: bool, load_in_8bit: bool):
    from transformers import BitsAndBytesConfig
    if load_in_4bit:
        return BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype="bfloat16",
                                  bnb_4bit_quant_type="nf4", bnb_4bit_use_double_quant=True)
    if load_in_8bit:
        return BitsAndBytesConfig(load_in_8bit=True)
    return None


def run_sft(model, tokenizer, args):
    from trl import SFTConfig, SFTTrainer
    path = _resolve(SFT_DATA)
    print(f"[v15][SFT] loading {path}")
    train_rows = [json.loads(l) for l in open(path, "r", encoding="utf-8") if l.strip()]

    cfg = SFTConfig(
        output_dir=ADAPTER_DIR,
        per_device_train_batch_size=args.batch,
        gradient_accumulation_steps=args.grad_accum,
        num_train_epochs=args.sft_epochs,
        learning_rate=2e-4,
        max_seq_length=1024,
        logging_steps=25,
        save_strategy="epoch",
        gradient_checkpointing=True,
        bf16=True,
        report_to="none",
    )
    trainer = SFTTrainer(model=model, args=cfg, tokenizer=tokenizer, train_dataset=train_rows)
    trainer.train()
    trainer.save_model(ADAPTER_DIR)
    print(f"[v15][SFT] adapter saved -> {ADAPTER_DIR}")


def run_dpo(model, tokenizer, args):
    from trl import DPOConfig, DPOTrainer
    path = _resolve(DPO_DATA)
    print(f"[v15][DPO] loading {path}")
    dpo_rows = [json.loads(l) for l in open(path, "r", encoding="utf-8") if l.strip()]

    cfg = DPOConfig(
        output_dir=ADAPTER_DIR,
        per_device_train_batch_size=args.batch,
        gradient_accumulation_steps=args.grad_accum,
        num_train_epochs=args.dpo_epochs,
        learning_rate=5e-5,
        beta=0.1,
        max_prompt_length=512,
        max_length=1024,
        logging_steps=25,
        save_strategy="epoch",
        gradient_checkpointing=True,
        bf16=True,
        report_to="none",
    )
    trainer = DPOTrainer(model=model, args=cfg, tokenizer=tokenizer, train_dataset=dpo_rows)
    trainer.train()
    trainer.save_model(ADAPTER_DIR)
    print(f"[v15][DPO] adapter saved -> {ADAPTER_DIR}")


def merge_and_push(args):
    from peft import PeftModel
    from transformers import AutoModelForCausalLM, AutoTokenizer
    base = AutoModelForCausalLM.from_pretrained(BASE_MODEL, torch_dtype="bfloat16",
                                                device_map="auto")
    tok = AutoTokenizer.from_pretrained(BASE_MODEL)
    model = PeftModel.from_pretrained(base, ADAPTER_DIR)
    merged = model.merge_and_unload()
    os.makedirs(MERGED_DIR, exist_ok=True)
    merged.save_pretrained(MERGED_DIR)
    tok.save_pretrained(MERGED_DIR)
    print(f"[v15][MERGE] merged model -> {MERGED_DIR}")
    if args.push:
        merged.push_to_hub(HF_REPO, revision="v15")
        tok.push_to_hub(HF_REPO, revision="v15")
        print(f"[v15][PUSH] pushed merged model to {HF_REPO}@v15")


def main():
    ap = argparse.ArgumentParser(description="NeuralAI v15 SFT+DPO trainer")
    ap.add_argument("--stage", choices=["sft", "dpo", "all"], default="all")
    ap.add_argument("--batch", type=int, default=2)
    ap.add_argument("--grad-accum", type=int, default=8)
    ap.add_argument("--sft-epochs", type=int, default=3)
    ap.add_argument("--dpo-epochs", type=int, default=2)
    ap.add_argument("--load-in-4bit", default="true")
    ap.add_argument("--load-in-8bit", default="false")
    ap.add_argument("--merge", action="store_true")
    ap.add_argument("--push", action="store_true")
    args = ap.parse_args()

    load_in_4bit = args.load_in_4bit.lower() == "true"
    load_in_8bit = args.load_in_8bit.lower() == "true"

    from transformers import AutoModelForCausalLM, AutoTokenizer
    qcfg = load_quantization(load_in_4bit, load_in_8bit)
    print(f"[v15] loading base {BASE_MODEL} (4bit={load_in_4bit}, 8bit={load_in_8bit})")
    tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
    model = AutoModelForCausalLM.from_pretrained(
        BASE_MODEL, quantization_config=qcfg, device_map="auto", torch_dtype="bfloat16",
    )
    model.config.use_cache = False

    if args.stage in ("sft", "all"):
        run_sft(model, tokenizer, args)
    if args.stage in ("dpo", "all"):
        # reload adapter from SFT if we just ran SFT
        run_dpo(model, tokenizer, args)

    if args.merge or args.push:
        merge_and_push(args)

    print("[v15] done.")


if __name__ == "__main__":
    main()