File size: 5,836 Bytes
9d614f1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python3
"""RSS AI Engine - application entry point.

    python RSS_AI_Engine/main.py --mode clean       stream + clean data only
    python RSS_AI_Engine/main.py --mode pretrain    Stage-1 continual pre-training only
    python RSS_AI_Engine/main.py --mode finetune    Stage-2 rational SFT only
    python RSS_AI_Engine/main.py --mode train_all   purge -> fresh HF model -> clean -> pretrain -> finetune -> upload
    python RSS_AI_Engine/main.py --mode serve       load the trained model -> Telegram bot or CLI chat

Extras:  --fresh-model  --upload-only  --skip-upload  --limit 150|full  --tier 1|2|3  --threads N
"""
from __future__ import annotations

import argparse
import importlib.util
import os
import signal
import subprocess
import sys
from datetime import datetime
from pathlib import Path

# Set native tokenizer/OpenMP limits BEFORE importing torch/transformers/tokenizers.
os.environ["TOKENIZERS_PARALLELISM"] = "false"
os.environ["OMP_NUM_THREADS"] = "2"
os.environ["MKL_NUM_THREADS"] = "2"

ROOT = Path(__file__).resolve().parent

ENV_TEMPLATE = """HF_TOKEN="hf_PUT_YOUR_TOKEN_HERE"
TELEGRAM_BOT_TOKEN=""
TELEGRAM_CHAT_ID=""
BASE_MODEL_REPO="Qwen/Qwen2.5-0.5B-Instruct"
OUTPUT_HF_REPO="Biraboro/rss-ai-trained"
SOURCE_REPOS='[{"repo": "Biraboro/Youtop-trenscprit", "limit": 150}]'
LOCAL_CHAT_FILE="./chat.jsonl"
JSON_PRETRAIN_QUOTA="0.15"
PRETRAIN_CONV_RATIO="0.25"
PURGE_OLD_MODEL_ON_START="true"
EXTRA_BLOCKED_EXTENSIONS=""
"""

# (import name, pip name)
REQUIRED = [("torch", "torch"), ("transformers", "transformers"), ("accelerate", "accelerate"),
            ("peft", "peft"), ("datasets", "datasets"), ("huggingface_hub", "huggingface_hub"),
            ("safetensors", "safetensors"), ("psutil", "psutil"), ("dotenv", "python-dotenv"),
            ("requests", "requests")]


def ensure_env_file() -> None:
    env = ROOT / ".env"
    if not env.exists():
        env.write_text(ENV_TEMPLATE, encoding="utf-8")
        print(f"[Setup] created {env} - put your HF_TOKEN (write access) in it before uploading")


def _pip(package: str) -> None:
    base = [sys.executable, "-m", "pip", "install", "-q", package]
    if subprocess.call(base) != 0:
        # PEP 668 "externally managed" interpreters (Debian/Ubuntu system python) need this flag
        subprocess.check_call(base + ["--break-system-packages"])


def _cuda_present() -> bool:
    try:
        import torch
        return bool(torch.cuda.is_available())
    except Exception:
        return False


def ensure_dependencies() -> None:
    missing = [pip for mod, pip in REQUIRED if importlib.util.find_spec(mod) is None]
    for package in missing:
        print(f"[Setup] installing {package} ...")
        _pip(package)
    if _cuda_present() and importlib.util.find_spec("bitsandbytes") is None:
        print("[Setup] CUDA detected -> installing bitsandbytes (4-bit NF4)")
        try:
            _pip("bitsandbytes")
        except Exception as exc:
            print(f"[Setup] bitsandbytes install failed ({exc}); GPU tier will run without 4-bit")
    if not missing:
        print("[Setup] all dependencies present")


def parse_args(argv=None) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="RSS AI Engine", formatter_class=argparse.RawTextHelpFormatter)
    parser.add_argument("--mode", choices=["clean", "pretrain", "finetune", "train_all", "serve"], default="serve",
                        help="clean | pretrain | finetune | train_all | serve   (default: serve)")
    parser.add_argument("--fresh-model", action="store_true",
                        help="purge old checkpoints/offload files and download the base model again")
    parser.add_argument("--upload-only", action="store_true", help="only upload ./trained_model to OUTPUT_HF_REPO")
    parser.add_argument("--skip-upload", action="store_true", help="train_all without the final upload")
    parser.add_argument("--limit", default=None, help="override every repo limit: a number or 'full'")
    parser.add_argument("--tier", type=int, choices=[1, 2, 3], default=None, help="force execution tier")
    parser.add_argument("--threads", type=int, default=None, help="CPU threads for torch")
    parser.add_argument("--no-install", action="store_true", help="do not auto-install missing dependencies")
    return parser.parse_args(argv)


def _handle_sigterm(signum, _frame) -> None:
    stamp = datetime.now().astimezone().isoformat(timespec="seconds")
    print(f"\n[Runtime] received SIGTERM ({signum}) at {stamp}; "
          "external supervisor/timeout or user termination is possible; sender is not identifiable here.",
          flush=True)
    raise SystemExit(128 + int(signum))


def main(argv=None) -> int:
    try:
        signal.signal(signal.SIGTERM, _handle_sigterm)
    except (AttributeError, ValueError):
        pass
    args = parse_args(argv)
    ensure_env_file()
    if not args.no_install:
        ensure_dependencies()
    if str(ROOT) not in sys.path:
        sys.path.insert(0, str(ROOT))
    try:
        import torch
        torch.set_num_threads(2)
        try:
            torch.set_num_interop_threads(2)
        except RuntimeError:
            # Inter-op pool may already be initialised by an embedding host.
            pass
        print(f"[Runtime] torch threads={torch.get_num_threads()} interop={torch.get_num_interop_threads()}",
              flush=True)
    except ImportError:
        pass
    from Integration_Layer import engine_orchestrator as orchestrator
    cfg = orchestrator.build_config(args)
    try:
        orchestrator.run(args.mode, cfg, args)
    except FileNotFoundError as exc:
        print(f"\n[RSS] {exc}")
        return 2
    except KeyboardInterrupt:
        print("\n[RSS] interrupted")
        return 130
    return 0


if __name__ == "__main__":
    sys.exit(main())