RRas-ai-engine / RSS_AI_Engine /Integration_Layer /engine_orchestrator.py
Biraboro's picture
Upload folder using huggingface_hub
9d614f1 verified
Raw History Blame Contribute Delete
14.4 kB
"""Integration layer: the ONLY place that wires the modules together.
It imports exactly four public interfaces - data_api, hardware_api, training_api, serving_api - and never
touches anything inside an `internal/` folder.
Purge policy (PURGE_OLD_MODEL_ON_START=true or --fresh-model):
train_all / pretrain start a NEW training lineage -> old artifacts are purged and the base model re-downloaded
clean / finetune / serve never purge (finetune needs the stage-1 checkpoint,
serve needs the trained model) unless --fresh-model is given
"""
from __future__ import annotations
import json
import os
import sys
import time
from pathlib import Path
from typing import Dict, List, Optional
ROOT = Path(__file__).resolve().parent.parent
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from Data_Module import data_api # noqa: E402
from Hardware_Module import hardware_api # noqa: E402
from Serving_Module import serving_api # noqa: E402
from Training_Module import training_api # noqa: E402
DEFAULT_SOURCES = [{"repo": "Biraboro/Youtop-trenscprit", "limit": 150}]
# ------------------------------------------------------------------ configuration
def _path(root: Path, value: str) -> Path:
p = Path(value).expanduser()
return p if p.is_absolute() else (root / p).resolve()
def _parse_sources(raw: str) -> List[Dict]:
raw = (raw or "").strip()
if not raw:
return list(DEFAULT_SOURCES)
try:
data = json.loads(raw)
except ValueError:
data = [{"repo": part.strip(), "limit": 150} for part in raw.split(",") if part.strip()]
if isinstance(data, dict):
data = [data]
out = []
for item in data if isinstance(data, list) else []:
if isinstance(item, str):
out.append({"repo": item, "limit": 150})
elif isinstance(item, dict) and item.get("repo"):
out.append({"repo": str(item["repo"]), "limit": item.get("limit", 150)})
if not out:
print("[Config] SOURCE_REPOS could not be parsed -> using the default repo")
return list(DEFAULT_SOURCES)
return out
def _flag(value: str, default: bool = False) -> bool:
return str(value if value is not None else default).strip().lower() in ("1", "true", "yes", "y", "on")
def _float(value: str, default: float) -> float:
try:
return float(value)
except (TypeError, ValueError):
return default
def build_config(args=None) -> Dict:
env_file = ROOT / ".env"
try:
from dotenv import load_dotenv
load_dotenv(env_file)
except ImportError: # minimal fallback parser
if env_file.exists():
for line in env_file.read_text(encoding="utf-8").splitlines():
if "=" in line and not line.strip().startswith("#"):
key, _, val = line.partition("=")
os.environ.setdefault(key.strip(), val.strip().strip("'\""))
env = os.environ.get
tune: Dict = {}
raw_tune = env("TUNE_JSON", "").strip()
if raw_tune:
try:
tune = json.loads(raw_tune)
except ValueError:
print("[Config] TUNE_JSON is not valid JSON - ignored")
extra = [e.strip() for e in (env("EXTRA_BLOCKED_EXTENSIONS", "") or "").replace(";", ",").split(",") if e.strip()]
cfg: Dict = {
"root": str(ROOT),
"hf_token": (env("HF_TOKEN", "") or "").strip(),
"telegram_token": (env("TELEGRAM_BOT_TOKEN", "") or "").strip(),
"telegram_chat_id": (env("TELEGRAM_CHAT_ID", "") or "").strip(),
"base_model_repo": (env("BASE_MODEL_REPO", "Qwen/Qwen2.5-0.5B-Instruct") or "").strip(),
"output_hf_repo": (env("OUTPUT_HF_REPO", "Biraboro/rss-ai-trained") or "").strip(),
"source_repos": _parse_sources(env("SOURCE_REPOS", "")),
"chat_file": str(_path(ROOT, env("LOCAL_CHAT_FILE", "./chat.jsonl") or "./chat.jsonl")),
"json_quota": _float(env("JSON_PRETRAIN_QUOTA", "0.15"), 0.15),
"conv_ratio": _float(env("PRETRAIN_CONV_RATIO", "0.25"), 0.25),
"purge_old_model": _flag(env("PURGE_OLD_MODEL_ON_START", "true"), True),
"extra_blocked_extensions": extra,
"tune": tune,
"force_tier": None,
"cpu_threads": None,
"limit_override": None,
}
if args is not None:
if getattr(args, "limit", None):
cfg["limit_override"] = args.limit
if getattr(args, "tier", None):
cfg["force_tier"] = args.tier
if getattr(args, "threads", None):
cfg["cpu_threads"] = args.threads
return cfg
def _dirs(cfg: Dict) -> Dict[str, Path]:
root = Path(cfg["root"])
return {"stage1": root / "checkpoints" / "stage1", "final": root / "trained_model", "root": root}
def _valid_stage1_checkpoint(folder: Path) -> bool:
"""Validate Stage-1 metadata AND the actual weight payload before any resume decision."""
folder = Path(folder)
meta = training_api.read_meta(folder)
if not meta or int(meta.get("stage", 0)) != 1:
return False
kind = meta.get("kind")
if kind == "full":
if not (folder / "config.json").is_file():
return False
single = folder / "model.safetensors"
index = folder / "model.safetensors.index.json"
if single.is_file() and single.stat().st_size > 0:
return True
if index.is_file():
try:
weight_map = json.loads(index.read_text(encoding="utf-8")).get("weight_map", {})
shards = {str(name) for name in weight_map.values()}
return bool(shards) and all((folder / name).is_file() and (folder / name).stat().st_size > 0
for name in shards)
except Exception:
return False
return False
if kind == "adapter":
adapter = folder / (meta.get("adapter_dir") or "adapter")
if not (adapter / "adapter_config.json").is_file():
return False
return any(path.is_file() and path.stat().st_size > 0 for path in
(adapter / "adapter_model.safetensors", adapter / "adapter_model.bin"))
return False
def _banner(text: str) -> None:
print("\n" + "=" * 72 + f"\n {text}\n" + "=" * 72)
# ------------------------------------------------------------------ building blocks
def _should_purge(cfg: Dict, args, mode: str) -> bool:
# Stage-1 is a protected lineage checkpoint. Never let the old default PURGE=true,
# --fresh-model, or train_all remove ./checkpoints/stage1.
return False
def _load(model_dir: str, cfg: Dict, recipe: Optional[Dict] = None):
"""profile -> plan -> tokenizer surgery -> load -> finalize. Returns (model, tokenizer, plan, surgery)."""
prof = hardware_api.profile_system(model_dir, cfg)
plan = hardware_api.plan_execution(prof, cfg)
tokenizer, surgery = training_api.load_tokenizer(model_dir, recipe=recipe)
model = hardware_api.load_model(model_dir, plan, cfg, row_recipe=surgery["recipe"] or None)
training_api.finalize_model(model, tokenizer, surgery)
return model, tokenizer, plan, surgery
def _ensure_clean(cfg: Dict) -> None:
if not data_api.cleaned_data_ready(cfg):
print("[Orchestrator] cleaned data not found -> running the clean step first")
data_api.run_clean(cfg)
# ------------------------------------------------------------------ modes
def mode_clean(cfg: Dict, args=None) -> Dict:
_banner("MODE clean - stream + clean data")
data_api.ensure_chat_file(cfg)
return data_api.run_clean(cfg)
def mode_pretrain(cfg: Dict, args=None, model_dir: Optional[str] = None) -> Dict:
_banner("MODE pretrain - Stage 1 continual pre-training")
dirs = _dirs(cfg)
if _valid_stage1_checkpoint(dirs["stage1"]):
meta = training_api.read_meta(dirs["stage1"]) or {}
print(f"[Orchestrator] valid Stage-1 checkpoint found -> SKIP Stage 1; resume from {dirs['stage1']}",
flush=True)
return {"kind": meta.get("kind"), "checkpoint": str(dirs["stage1"]), "skipped": True}
if dirs["stage1"].exists():
raise RuntimeError(f"Stage-1 directory exists but failed validation; refusing to overwrite/delete it: {dirs['stage1']}")
data_api.ensure_chat_file(cfg)
_ensure_clean(cfg)
dirs = _dirs(cfg)
if model_dir is None:
model_dir = hardware_api.prepare_base_model(cfg, fresh=False)
paths = data_api.cleaned_paths(cfg)
model, tokenizer, plan, surgery = _load(model_dir, cfg)
try:
result = training_api.run_pretrain(model, tokenizer, plan, cfg, surgery, paths["pretrain"], dirs["stage1"])
finally:
del model
hardware_api.release_memory()
print(f"[Orchestrator] Stage 1 done (loss {result['loss']:.4f}) -> {result['checkpoint']}")
return result
def mode_finetune(cfg: Dict, args=None, model_dir: Optional[str] = None) -> Dict:
_banner("MODE finetune - Stage 2 rational SFT")
data_api.ensure_chat_file(cfg)
_ensure_clean(cfg)
dirs = _dirs(cfg)
paths = data_api.cleaned_paths(cfg)
meta = training_api.read_meta(dirs["stage1"])
if dirs["stage1"].exists() and not _valid_stage1_checkpoint(dirs["stage1"]):
raise RuntimeError(f"Stage-1 directory exists but checkpoint metadata/weights are missing/invalid; refusing fallback or overwrite: {dirs['stage1']}")
adapter_dir: Optional[str] = None
recipe = None
if meta is None:
print("[Orchestrator] no Stage-1 checkpoint found -> fine-tuning directly from the base model")
source = model_dir or hardware_api.prepare_base_model(cfg, fresh=False)
elif meta["kind"] == "adapter":
source = model_dir or hardware_api.prepare_base_model(cfg, fresh=False)
adapter_dir = str(dirs["stage1"] / meta["adapter_dir"])
recipe = meta.get("row_recipe") or None
print(f"[Orchestrator] continuing the Stage-1 LoRA adapter on top of {cfg['base_model_repo']}")
else:
source = str(dirs["stage1"])
recipe = meta.get("row_recipe") or None
print(f"[Orchestrator] loading the Stage-1 checkpoint {source}")
model, tokenizer, plan, surgery = _load(source, cfg, recipe=recipe)
try:
result = training_api.run_finetune(model, tokenizer, plan, cfg, surgery, paths["sft"], dirs["final"],
adapter_dir=adapter_dir)
finally:
del model
hardware_api.release_memory()
print(f"[Orchestrator] Stage 2 done (loss {result['loss']:.4f}) -> {result['final_dir']}")
return result
def mode_upload(cfg: Dict, args=None) -> bool:
final = _dirs(cfg)["final"]
ok = training_api.upload_model(final, cfg)
if ok:
training_api.clear_cache(cfg["root"])
return ok
def mode_train_all(cfg: Dict, args=None) -> Dict:
_banner("MODE train_all - checkpoint-aware clean -> pretrain/skip -> finetune -> upload")
started = time.time()
data_api.ensure_chat_file(cfg)
dirs = _dirs(cfg)
stage1_valid = _valid_stage1_checkpoint(dirs["stage1"])
if dirs["stage1"].exists() and not stage1_valid:
raise RuntimeError(f"Stage-1 directory exists but is not a valid checkpoint; preserving it and refusing overwrite: {dirs['stage1']}")
if stage1_valid:
stage1_meta = training_api.read_meta(dirs["stage1"]) or {}
print(f"[1/5] Stage-1 checkpoint verified ({stage1_meta.get('kind')}) -> preserving; no purge/download", flush=True)
model_dir = None
if stage1_meta.get("kind") == "adapter":
model_dir = hardware_api.prepare_base_model(cfg, fresh=False)
else:
print("[1/5] base model (reuse verified local copy; destructive purge disabled)", flush=True)
model_dir = hardware_api.prepare_base_model(cfg, fresh=False)
print("[2/5] cleaning data", flush=True)
if data_api.cleaned_data_ready(cfg):
print("[Data] cleaned SFT/pretrain files already exist -> reuse", flush=True)
else:
data_api.run_clean(cfg)
if stage1_valid:
print(f"[3/5] stage 1 -> SKIPPED (verified checkpoint {dirs['stage1']})", flush=True)
else:
print("[3/5] stage 1 -> train once (checkpoint path is write-once)", flush=True)
mode_pretrain(cfg, args, model_dir=model_dir)
print("[4/5] stage 2 -> rational SFT", flush=True)
result = mode_finetune(cfg, args, model_dir=model_dir)
uploaded = False
if getattr(args, "skip_upload", False):
print("[5/5] upload skipped (--skip-upload)", flush=True)
else:
print("[5/5] uploading to Hugging Face", flush=True)
uploaded = mode_upload(cfg, args)
result["uploaded"] = uploaded
print(f"\n[Orchestrator] train_all finished in {(time.time() - started) / 60:.1f} min "
f"(uploaded={uploaded}). Chat with it: python RSS_AI_Engine/main.py --mode serve", flush=True)
return result
def mode_serve(cfg: Dict, args=None) -> str:
_banner("MODE serve - load trained model and start the chat front-end")
data_api.ensure_chat_file(cfg)
trained = serving_api.resolve_trained_dir(cfg, fetch_remote=training_api.download_trained)
meta = training_api.read_meta(trained) or {}
if meta.get("has_adapter") and not meta.get("base_is_self"):
weights_dir = hardware_api.prepare_base_model(cfg, fresh=False)
else:
weights_dir = str(trained)
recipe = meta.get("row_recipe") or None
cfg["active_vocab_file"] = str(trained / "active_vocab_ids.json")
model, tokenizer, plan, surgery = _load(weights_dir, cfg, recipe=recipe)
if weights_dir != str(trained): # adapter-only: tokenizer must be the trained one
tokenizer, surgery = training_api.load_tokenizer(str(trained), recipe=recipe)
model = serving_api.prepare_model(model, trained, meta)
engine = serving_api.build_engine(model, tokenizer, cfg)
return serving_api.start(engine, cfg)
def run(mode: str, cfg: Dict, args=None):
table = {"clean": mode_clean, "pretrain": mode_pretrain, "finetune": mode_finetune,
"train_all": mode_train_all, "serve": mode_serve}
if getattr(args, "upload_only", False):
return mode_upload(cfg, args)
return table[mode](cfg, args)