MamunAI / hf_space_train.py
almamunkhan's picture
Upldate
835e758 verified
Raw
History Blame Contribute Delete
13.1 kB
"""
╔══════════════════════════════════════════════════════════════╗
β•‘ MamunAI β€” Hugging Face Spaces (ZeroGPU) Training β•‘
β•‘ Owner : Al Mamun Khan β•‘
β•‘ β•‘
β•‘ HOW TO USE: β•‘
β•‘ 1. Create a new HF Space (Gradio, ZeroGPU hardware) β•‘
β•‘ 2. Upload: hf_space_train.py, dataset.jsonl, requirements β•‘
β•‘ 3. Click "Run Training" button in the UI β•‘
β•‘ 4. Download GGUF from Files tab when done β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
"""
import gradio as gr
import json, os, shutil, subprocess, threading, time
from pathlib import Path
# ── Config ────────────────────────────────────────────────────────────────────
BASE_MODEL = "HuggingFaceTB/SmolLM-360M-Instruct"
ADAPTER_DIR = "./lora_adapter"
MERGED_DIR = "./merged_model"
GGUF_OUT = "./MamunAI-finetuned.gguf"
DATASET_FILE = "./dataset.jsonl"
LORA_RANK = 16
LORA_ALPHA = 32
LORA_MODULES = ["q_proj", "k_proj", "v_proj", "o_proj"]
BATCH_SIZE = 16 # A100 β†’ can go higher; T4 free tier β†’ 16 safe
EPOCHS = 3
LR = 2e-4
MAX_SEQ_LEN = 256
SYSTEM = (
"You are an Android phone control assistant. You do not possess any internal data. "
"Your sole function is to convert user input into JSON commands. "
"You understand English. You have access to tools:\n"
"1. android_control\n2. web_search\n3. direct_answer\n4. clarify\n\n"
"Your output must be strictly in JSON format.\n\n"
'Example: User: "Dim the lights" -> Output: {"tool": "android_control", "action": "set_brightness", "params": {"value": 30}}\n'
'Example: User: "Today\'s weather?" -> Output: {"tool": "web_search", "query": "Dhaka weather 2026"}\n'
'Example: User: "Hello" -> Output: {"tool": "direct_answer", "response": "I am doing well!"}\n\n'
"Now process:"
)
# ── Training log stream ───────────────────────────────────────────────────────
_log_lines = []
_training_done = False
_gguf_ready = False
def log(msg):
_log_lines.append(msg)
print(msg)
def get_logs():
return "\n".join(_log_lines[-80:]) # last 80 lines
# ── Core training function ────────────────────────────────────────────────────
def run_training(dataset_file):
global _training_done, _gguf_ready, _log_lines
_log_lines = []
_training_done = False
_gguf_ready = False
try:
import torch
from transformers import (AutoTokenizer, AutoModelForCausalLM,
BitsAndBytesConfig, TrainingArguments)
from peft import LoraConfig, get_peft_model, TaskType, prepare_model_for_kbit_training
from trl import SFTTrainer
from datasets import Dataset
# ── GPU check ──────────────────────────────────────────────────────
if not torch.cuda.is_available():
log("❌ No GPU! Please use ZeroGPU Space or T4 runtime.")
return
log(f"βœ… GPU: {torch.cuda.get_device_name(0)}")
log(f" VRAM: {torch.cuda.get_device_properties(0).total_memory/1e9:.1f} GB")
# ── Load dataset ───────────────────────────────────────────────────
ds_path = dataset_file if dataset_file else DATASET_FILE
texts = []
with open(ds_path) as f:
for line in f:
line = line.strip()
if not line: continue
rec = json.loads(line)
text = (
f"<|im_start|>system\n{SYSTEM.strip()}<|im_end|>\n"
f"<|im_start|>user\nUser: \"{rec['instruction']}\"<|im_end|>\n"
f"<|im_start|>assistant\n{rec['output']}<|im_end|>"
)
texts.append(text)
log(f"πŸ“‚ Loaded {len(texts):,} examples")
# ── Tokenizer ──────────────────────────────────────────────────────
log(f"⬇️ Loading tokenizer...")
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained(BASE_MODEL)
if tok.pad_token is None:
tok.pad_token = tok.eos_token
tok.padding_side = "right"
# ── Model (4-bit QLoRA) ────────────────────────────────────────────
log(f"⬇️ Loading model (4-bit QLoRA)...")
bnb = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.float16,
)
model = AutoModelForCausalLM.from_pretrained(
BASE_MODEL, quantization_config=bnb,
device_map="auto", low_cpu_mem_usage=True,
)
model.config.use_cache = False
# ── LoRA ───────────────────────────────────────────────────────────
model = prepare_model_for_kbit_training(model, use_gradient_checkpointing=True)
lora_cfg = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=LORA_RANK, lora_alpha=LORA_ALPHA,
lora_dropout=0.05, target_modules=LORA_MODULES,
bias="none", inference_mode=False,
)
model = get_peft_model(model, lora_cfg)
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
log(f"πŸ”§ Trainable params: {trainable/1e6:.2f}M")
# ── Train ──────────────────────────────────────────────────────────
os.makedirs(ADAPTER_DIR, exist_ok=True)
dataset = Dataset.from_dict({"text": texts})
args = TrainingArguments(
output_dir=ADAPTER_DIR,
num_train_epochs=EPOCHS,
per_device_train_batch_size=BATCH_SIZE,
gradient_accumulation_steps=2,
gradient_checkpointing=True,
warmup_ratio=0.03,
learning_rate=LR,
lr_scheduler_type="cosine",
fp16=True, bf16=False,
optim="paged_adamw_8bit",
logging_steps=10,
save_steps=500,
save_total_limit=1,
report_to="none",
group_by_length=True,
)
trainer = SFTTrainer(
model=model, tokenizer=tok,
train_dataset=dataset,
dataset_text_field="text",
max_seq_length=MAX_SEQ_LEN,
packing=True, args=args,
)
log("πŸš€ Training started...")
trainer.train()
# ── Save adapter ───────────────────────────────────────────────────
model.save_pretrained(ADAPTER_DIR)
tok.save_pretrained(ADAPTER_DIR)
log(f"πŸ’Ύ Adapter saved β†’ {ADAPTER_DIR}/")
# ── Merge ──────────────────────────────────────────────────────────
log("πŸ”€ Merging LoRA into base model...")
from peft import PeftModel
base = AutoModelForCausalLM.from_pretrained(
BASE_MODEL, torch_dtype=torch.float16, low_cpu_mem_usage=True
)
peft_m = PeftModel.from_pretrained(base, ADAPTER_DIR)
merged = peft_m.merge_and_unload()
os.makedirs(MERGED_DIR, exist_ok=True)
merged.save_pretrained(MERGED_DIR)
tok.save_pretrained(MERGED_DIR)
log(f"βœ… Merged model β†’ {MERGED_DIR}/")
# ── GGUF ───────────────────────────────────────────────────────────
log("βš™οΈ Converting to GGUF Q4_K_M...")
llama_dir = "/tmp/llama.cpp"
if not Path(llama_dir).exists():
subprocess.run(["git","clone","--depth","1",
"https://github.com/ggerganov/llama.cpp", llama_dir],
check=True, capture_output=True)
f16 = "/tmp/mamunai-f16.gguf"
subprocess.run(
["python3", f"{llama_dir}/convert_hf_to_gguf.py",
MERGED_DIR, "--outfile", f16, "--outtype", "f16"],
check=True
)
import ctypes, llama_cpp as lc
params = lc.llama_model_quantize_default_params()
params.ftype = 15 # Q4_K_M
params.nthread = 4
lc.llama_model_quantize(f16.encode(), GGUF_OUT.encode(), ctypes.byref(params))
Path(f16).unlink(missing_ok=True)
size = Path(GGUF_OUT).stat().st_size / 1e6
log(f"βœ… GGUF ready β†’ {GGUF_OUT} ({size:.0f} MB)")
_gguf_ready = True
_training_done = True
log("πŸŽ‰ ALL DONE! Download MamunAI-finetuned.gguf from Files tab.")
except Exception as e:
import traceback
log(f"❌ Error: {e}")
log(traceback.format_exc())
_training_done = True
# ── Gradio UI ─────────────────────────────────────────────────────────────────
def start_training(dataset_upload):
global _log_lines, _training_done
_log_lines = ["⏳ Starting training..."]
_training_done = False
ds_path = dataset_upload if dataset_upload else DATASET_FILE
if not Path(ds_path).exists():
return "❌ dataset.jsonl not found. Please upload it.", None
t = threading.Thread(target=run_training, args=(ds_path,), daemon=True)
t.start()
return "βœ… Training started! Logs updating below...", None
def refresh_logs():
done_msg = "\n\nβœ… TRAINING COMPLETE β€” download GGUF from Files tab!" if _training_done else ""
return get_logs() + done_msg
def download_gguf():
if Path(GGUF_OUT).exists():
return GGUF_OUT
return None
with gr.Blocks(title="MamunAI Trainer", theme=gr.themes.Soft()) as demo:
gr.Markdown("""
# πŸ€– MamunAI Trainer β€” Hugging Face Space
**Owner:** Al Mamun Khan | Model: SmolLM-360M-Instruct + LoRA Q4_K_M
> Upload your `dataset.jsonl` and click **Start Training**.
> Training takes ~45–60 min on A100, ~60–90 min on T4.
""")
with gr.Row():
with gr.Column(scale=1):
dataset_file = gr.File(
label="πŸ“‚ Upload dataset.jsonl",
file_types=[".jsonl"],
)
train_btn = gr.Button("πŸš€ Start Training", variant="primary", size="lg")
status_box = gr.Textbox(label="Status", lines=2, interactive=False)
refresh_btn = gr.Button("πŸ”„ Refresh Logs", size="sm")
download_btn = gr.DownloadButton(
label="⬇️ Download MamunAI-finetuned.gguf",
visible=True,
)
with gr.Column(scale=2):
log_box = gr.Textbox(
label="πŸ“‹ Training Logs",
lines=30,
max_lines=30,
interactive=False,
autoscroll=True,
)
# ── Wiring ────────────────────────────────────────────────────────────────
train_btn.click(
fn=start_training,
inputs=[dataset_file],
outputs=[status_box, log_box],
)
refresh_btn.click(fn=refresh_logs, outputs=[log_box])
download_btn.click(fn=download_gguf, outputs=[download_btn])
gr.Markdown("""
---
### βš™οΈ Training Config
| Setting | Value |
|---------|-------|
| Base Model | SmolLM-360M-Instruct |
| LoRA Rank | 16 |
| Target Modules | q, k, v, o projections |
| Batch Size | 16 |
| Epochs | 3 |
| Optimizer | paged_adamw_8bit |
| Precision | FP16 |
| Output | Q4_K_M GGUF (~260 MB) |
""")
if __name__ == "__main__":
demo.launch()