Alibrown commited on
Commit
b57b225
·
verified ·
1 Parent(s): f6e3f95

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +179 -0
app.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import os
3
+ import re
4
+ import tempfile
5
+ import time
6
+
7
+ import gradio as gr
8
+ import spaces
9
+ import torch
10
+ from datasets import load_dataset
11
+ from huggingface_hub import HfApi
12
+ from peft import LoraConfig, get_peft_model
13
+ from transformers import AutoModelForCausalLM, AutoTokenizer
14
+
15
+ BASE_MODELS = [
16
+ "HuggingFaceTB/SmolLM2-135M-Instruct",
17
+ "HuggingFaceTB/SmolLM2-360M-Instruct",
18
+ "HuggingFaceTB/SmolLM2-1.7B-Instruct",
19
+ ]
20
+ MAX_LEN = 384
21
+ BATCH_SIZE = 8
22
+
23
+
24
+ def clean_id(s: str) -> str:
25
+ """Akzeptiert URL oder ID: https://huggingface.co/datasets/user/name -> user/name"""
26
+ s = s.strip().rstrip("/")
27
+ s = re.sub(r"^https?://huggingface\.co/", "", s)
28
+ s = re.sub(r"^(datasets|models)/", "", s)
29
+ s = re.sub(r"/(tree|blob|viewer)/.*$", "", s)
30
+ return s
31
+
32
+
33
+ def build_texts(ds, tok, text_col):
34
+ cols = ds.column_names
35
+ texts = []
36
+ for row in ds:
37
+ if "messages" in cols:
38
+ t = tok.apply_chat_template(row["messages"], tokenize=False)
39
+ elif text_col and text_col in cols:
40
+ t = str(row[text_col])
41
+ elif "text" in cols:
42
+ t = str(row["text"])
43
+ else:
44
+ q = next((row[c] for c in ("instruction", "prompt", "question", "input") if c in cols and row[c]), None)
45
+ a = next((row[c] for c in ("output", "response", "answer", "completion") if c in cols and row[c]), None)
46
+ if q is None or a is None:
47
+ raise gr.Error(
48
+ f"Spalten nicht erkannt: {cols}. Nutze 'messages', 'text' oder instruction/output "
49
+ "(oder gib den Spaltennamen an)."
50
+ )
51
+ t = tok.apply_chat_template(
52
+ [{"role": "user", "content": str(q)}, {"role": "assistant", "content": str(a)}],
53
+ tokenize=False,
54
+ )
55
+ texts.append(t)
56
+ return texts
57
+
58
+
59
+ def gpu_duration(model, input_ids, attention_mask, seconds, epochs, lr, out_dir):
60
+ return int(seconds) + 20
61
+
62
+
63
+ @spaces.GPU(duration=gpu_duration)
64
+ def train_gpu(model, input_ids, attention_mask, seconds, epochs, lr, out_dir):
65
+ model.to("cuda")
66
+ model.train()
67
+ params = [p for p in model.parameters() if p.requires_grad]
68
+ opt = torch.optim.AdamW(params, lr=lr)
69
+ n = input_ids.size(0)
70
+ start = time.time()
71
+ losses, stop = [], False
72
+ for _ in range(int(epochs)):
73
+ perm = torch.randperm(n)
74
+ for i in range(0, n, BATCH_SIZE):
75
+ if time.time() - start > seconds:
76
+ stop = True
77
+ break
78
+ idx = perm[i : i + BATCH_SIZE]
79
+ ids = input_ids[idx].to("cuda")
80
+ am = attention_mask[idx].to("cuda")
81
+ labels = ids.clone()
82
+ labels[am == 0] = -100
83
+ loss = model(input_ids=ids, attention_mask=am, labels=labels).loss
84
+ loss.backward()
85
+ opt.step()
86
+ opt.zero_grad()
87
+ losses.append(loss.item())
88
+ if stop:
89
+ break
90
+ model.save_pretrained(out_dir)
91
+ return losses, time.time() - start, stop
92
+
93
+
94
+ def run(
95
+ base, dataset_in, split, text_col, max_examples, epochs, seconds, lr, rank,
96
+ target_repo, manual_token, profile: gr.OAuthProfile | None, oauth_token: gr.OAuthToken | None,
97
+ progress=gr.Progress(),
98
+ ):
99
+ token = (manual_token or "").strip() or (oauth_token.token if oauth_token else None)
100
+ if not token:
101
+ raise gr.Error("Bitte oben mit Hugging Face einloggen (oder Token eingeben).")
102
+ api = HfApi(token=token)
103
+ user = profile.username if profile else api.whoami()["name"]
104
+ dataset_id = clean_id(dataset_in)
105
+ if not dataset_id:
106
+ raise gr.Error("Dataset-URL oder -ID fehlt.")
107
+ repo = clean_id(target_repo) or f"{user}/{base.split('/')[-1]}-ft"
108
+
109
+ yield "Lade Tokenizer & Dataset (CPU, zählt nicht zur GPU-Zeit) ..."
110
+ tok = AutoTokenizer.from_pretrained(base)
111
+ if tok.pad_token is None:
112
+ tok.pad_token = tok.eos_token
113
+ ds = load_dataset(dataset_id, split=f"{split}[:{int(max_examples)}]", token=token)
114
+ texts = build_texts(ds, tok, text_col)
115
+ enc = tok(texts, truncation=True, max_length=MAX_LEN, padding="max_length", return_tensors="pt")
116
+
117
+ yield f"{len(texts)} Beispiele bereit. Lade Modell + LoRA (CPU) ..."
118
+ model = AutoModelForCausalLM.from_pretrained(base, torch_dtype=torch.bfloat16)
119
+ cfg = LoraConfig(
120
+ r=int(rank), lora_alpha=int(rank) * 2, lora_dropout=0.05,
121
+ target_modules="all-linear", task_type="CAUSAL_LM",
122
+ )
123
+ model = get_peft_model(model, cfg)
124
+
125
+ out_dir = tempfile.mkdtemp()
126
+ yield f"Training startet (max. {int(seconds)} s GPU-Zeit) ..."
127
+ losses, took, stopped = train_gpu(
128
+ model, enc["input_ids"], enc["attention_mask"], float(seconds), int(epochs), float(lr), out_dir
129
+ )
130
+ if not losses:
131
+ raise gr.Error("Kein Trainingsschritt gelaufen (GPU-Zeit zu kurz?).")
132
+ first = sum(losses[:5]) / len(losses[:5])
133
+ last = sum(losses[-5:]) / len(losses[-5:])
134
+
135
+ yield f"Fertig trainiert ({len(losses)} Steps, {took:.0f} s). Lade hoch nach {repo} ..."
136
+ api.create_repo(repo, exist_ok=True, repo_type="model")
137
+ with open(os.path.join(out_dir, "README.md"), "w") as f:
138
+ f.write(
139
+ f"---\nbase_model: {base}\nlibrary_name: peft\ntags:\n- lora\n- zerogpu-trainer\n---\n"
140
+ f"LoRA-Adapter auf `{base}`, trainiert mit `{dataset_id}`.\n"
141
+ )
142
+ api.upload_folder(folder_path=out_dir, repo_id=repo, repo_type="model")
143
+ note = " (Zeitlimit erreicht, Training vorzeitig beendet)" if stopped else ""
144
+ yield (
145
+ f"✅ Fertig{note}\n"
146
+ f"Steps: {len(losses)} | Loss: {first:.3f} → {last:.3f}\n"
147
+ f"Adapter: https://huggingface.co/{repo}"
148
+ )
149
+
150
+
151
+ with gr.Blocks(title="ZeroGPU Mini-Trainer") as demo:
152
+ gr.Markdown(
153
+ "# ⚡ ZeroGPU Mini-Trainer\n"
154
+ "Fine-Tune ein kleines SmolLM per LoRA in wenigen Minuten – mit deiner eigenen ZeroGPU-Tagesquote."
155
+ )
156
+ gr.LoginButton()
157
+ with gr.Row():
158
+ base = gr.Dropdown(BASE_MODELS, value=BASE_MODELS[1], label="Basismodell")
159
+ dataset_in = gr.Textbox(label="Dataset (URL oder user/name)", placeholder="https://huggingface.co/datasets/...")
160
+ with gr.Row():
161
+ split = gr.Textbox(value="train", label="Split")
162
+ text_col = gr.Textbox(label="Textspalte (optional)", placeholder="leer = automatisch")
163
+ max_examples = gr.Slider(50, 5000, value=1000, step=50, label="Max. Beispiele")
164
+ with gr.Row():
165
+ epochs = gr.Slider(1, 5, value=2, step=1, label="Epochen")
166
+ seconds = gr.Slider(30, 240, value=150, step=10, label="Max. GPU-Sekunden")
167
+ lr = gr.Number(value=2e-4, label="Learning Rate")
168
+ rank = gr.Slider(4, 64, value=16, step=4, label="LoRA Rank")
169
+ target_repo = gr.Textbox(label="Ziel-Modell-Repo (leer = automatisch)", placeholder="dein-name/mein-modell")
170
+ manual_token = gr.Textbox(label="Optional: HF-Token (nur falls Login nicht geht)", type="password")
171
+ btn = gr.Button("Training starten", variant="primary")
172
+ out = gr.Textbox(label="Status", lines=6)
173
+ btn.click(
174
+ run,
175
+ [base, dataset_in, split, text_col, max_examples, epochs, seconds, lr, rank, target_repo, manual_token],
176
+ out,
177
+ )
178
+
179
+ demo.queue().launch()