Bc-AI commited on
Commit
aa96560
Β·
verified Β·
1 Parent(s): 149725c

Create App.py

Browse files
Files changed (1) hide show
  1. App.py +343 -0
App.py ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =============================================================================
2
+ # Full-Parameter SFT: Qwen/Qwen3.5-4B-Base on Bc-AI/SFT-Ultra
3
+ # On-the-fly streaming | Bad row filtering | HF Hub upload
4
+ # =============================================================================
5
+ # Requirements:
6
+
7
+ # pip install torch transformers datasets trl accelerate huggingface_hub
8
+
9
+ import os
10
+ import torch
11
+ from datasets import load_dataset
12
+ from transformers import AutoModelForCausalLM, AutoTokenizer
13
+ from trl import SFTTrainer, SFTConfig
14
+ from huggingface_hub import HfApi, login
15
+
16
+ # =============================================================================
17
+ # 0. CONFIG β€” Edit these as needed
18
+ # =============================================================================
19
+ MODEL_ID = "Qwen/Qwen3.5-4B"
20
+ DATASET_ID = "Bc-AI/SFT-Ultra"
21
+ OUTPUT_DIR = "./qwen3.5-4b-full-sft"
22
+
23
+ # HF Hub β€” paste your token when prompted at runtime
24
+ HF_REPO_ID = "Bc-AI/qwen3.5-4b-sft" # ← change this
25
+ HF_TOKEN = "" # Leave None β€” you will be prompted to paste it below
26
+
27
+ # Sequence
28
+ MAX_SEQ_LENGTH = 2048
29
+
30
+ # Training hyperparameters
31
+ NUM_TRAIN_EPOCHS = 1
32
+ PER_DEVICE_TRAIN_BATCH_SIZE = 2
33
+ GRADIENT_ACCUMULATION_STEPS = 8
34
+ LEARNING_RATE = 1e-5
35
+ WEIGHT_DECAY = 0.01
36
+ WARMUP_RATIO = 0.05
37
+ LR_SCHEDULER = "cosine"
38
+ MAX_GRAD_NORM = 1.0
39
+
40
+ # Logging & saving
41
+ LOGGING_STEPS = 10
42
+ SAVE_STEPS = 500
43
+ SAVE_TOTAL_LIMIT = 3
44
+
45
+ # Precision β€” bf16 on Ampere+ (A100, 3090, 4090)
46
+ # set bf16=False fp16=True on older GPUs (V100, T4)
47
+ USE_BF16 = True
48
+ USE_FP16 = False
49
+
50
+ # Streaming = on-the-fly download, no full disk pre-cache
51
+ STREAM_DATASET = True
52
+
53
+ SEED = 42
54
+
55
+ # =============================================================================
56
+ # 1. HF HUB LOGIN β€” paste token here at runtime
57
+ # =============================================================================
58
+ print("=" * 60)
59
+ print(" Hugging Face Hub Login")
60
+ print("=" * 60)
61
+
62
+ if HF_TOKEN is None:
63
+ HF_TOKEN = input(" Paste your HF token (hf_…): ").strip()
64
+
65
+ login(token=HF_TOKEN, add_to_git_credential=False)
66
+ print(" βœ… Logged in successfully.\n")
67
+
68
+ # =============================================================================
69
+ # 2. TOKENIZER
70
+ # =============================================================================
71
+ print(f"[1/4] Loading tokenizer: {MODEL_ID}")
72
+
73
+ tokenizer = AutoTokenizer.from_pretrained(
74
+ MODEL_ID,
75
+ trust_remote_code=True,
76
+ )
77
+
78
+ if tokenizer.pad_token is None:
79
+ tokenizer.pad_token = tokenizer.eos_token
80
+
81
+ tokenizer.padding_side = "right"
82
+
83
+ # =============================================================================
84
+ # 3. MODEL β€” full bf16, no quantisation, no adapter
85
+ # =============================================================================
86
+ print(f"[2/4] Loading full model: {MODEL_ID}")
87
+
88
+ model = AutoModelForCausalLM.from_pretrained(
89
+ MODEL_ID,
90
+ trust_remote_code=True,
91
+ torch_dtype=torch.bfloat16 if USE_BF16 else torch.float32,
92
+ device_map="auto",
93
+ )
94
+
95
+ model.config.use_cache = False
96
+
97
+ for param in model.parameters():
98
+ param.requires_grad = True
99
+
100
+ total_params = sum(p.numel() for p in model.parameters())
101
+ print(f" Trainable parameters: {total_params:,} ({total_params / 1e9:.2f}B)")
102
+
103
+ # =============================================================================
104
+ # 4. DATASET β€” streaming + bad row filtering + on-the-fly tokenisation
105
+ # =============================================================================
106
+ print(f"[3/4] Loading dataset: {DATASET_ID} (streaming={STREAM_DATASET})")
107
+
108
+ raw_dataset = load_dataset(
109
+ DATASET_ID,
110
+ split="train",
111
+ streaming=STREAM_DATASET,
112
+ trust_remote_code=True,
113
+ )
114
+
115
+ # ── Bad row validator ────────────────────────────────────────────────────────
116
+ # Catches every known failure mode so no single row can crash training
117
+
118
+ REQUIRED_ROLES = {"user", "assistant"} # Minimum roles a valid convo must have
119
+
120
+ def is_valid_row(example):
121
+ """
122
+ Returns True only if the row is safe to train on.
123
+ Filters out:
124
+ - Missing / non-list messages field
125
+ - Empty message list
126
+ - Messages with missing role or content keys
127
+ - Messages where role or content is not a string
128
+ - Messages where content is an empty / whitespace-only string
129
+ - Conversations missing at least one user AND one assistant turn
130
+ - Rows where the entire rendered text would be empty
131
+ """
132
+ try:
133
+ messages = example.get("messages", None)
134
+
135
+ # Must exist and be a non-empty list
136
+ if not isinstance(messages, list) or len(messages) == 0:
137
+ return False
138
+
139
+ seen_roles = set()
140
+ for msg in messages:
141
+ # Each message must be a dict
142
+ if not isinstance(msg, dict):
143
+ return False
144
+
145
+ role = msg.get("role", None)
146
+ content = msg.get("content", None)
147
+
148
+ # role and content must be non-empty strings
149
+ if not isinstance(role, str) or not role.strip():
150
+ return False
151
+ if not isinstance(content, str) or not content.strip():
152
+ return False
153
+
154
+ seen_roles.add(role.strip().lower())
155
+
156
+ # Must have at least one user turn and one assistant turn
157
+ if not REQUIRED_ROLES.issubset(seen_roles):
158
+ return False
159
+
160
+ return True
161
+
162
+ except Exception:
163
+ # Catch-all: any unexpected structure is silently dropped
164
+ return False
165
+
166
+
167
+ # ── Safe formatter ───────────────────────────────────────────────────────────
168
+
169
+ def format_messages(example):
170
+ """
171
+ Applies the Qwen3.5 chat template.
172
+ Wrapped in try/except so any template rendering failure is handled
173
+ gracefully β€” the row is marked with an empty text field and later dropped.
174
+ """
175
+ try:
176
+ text = tokenizer.apply_chat_template(
177
+ example["messages"],
178
+ tokenize=False,
179
+ add_generation_prompt=False,
180
+ )
181
+ # Final safety: rendered text must be non-trivial
182
+ if not text or not text.strip():
183
+ return {"text": ""}
184
+ return {"text": text}
185
+ except Exception:
186
+ return {"text": ""}
187
+
188
+
189
+ def is_non_empty_text(example):
190
+ """Drop any row where formatting produced an empty string."""
191
+ text = example.get("text", "")
192
+ return isinstance(text, str) and len(text.strip()) > 0
193
+
194
+
195
+ # ── Apply pipeline ───────────────────────────────────────────────────────────
196
+ print(" Step 1 β€” Filtering malformed rows …")
197
+ clean_dataset = raw_dataset.filter(is_valid_row)
198
+
199
+ print(" Step 2 β€” Applying chat template on the fly …")
200
+ formatted_dataset = clean_dataset.map(format_messages)
201
+
202
+ print(" Step 3 β€” Dropping any rows with empty rendered text …")
203
+ formatted_dataset = formatted_dataset.filter(is_non_empty_text)
204
+
205
+ print(" βœ… Dataset pipeline ready.\n")
206
+
207
+ # =============================================================================
208
+ # 5. TRAINER
209
+ # =============================================================================
210
+ print("[4/4] Configuring SFTTrainer …")
211
+
212
+ sft_config = SFTConfig(
213
+ # ── Output ───────────────────────────────────────────────────────────────
214
+ output_dir=OUTPUT_DIR,
215
+
216
+ # ── Sequence ─────────────────────────────────────────────────────────────
217
+ max_seq_length=MAX_SEQ_LENGTH,
218
+
219
+ # ── Training schedule ────────────────────────────────────────────────────
220
+ num_train_epochs=NUM_TRAIN_EPOCHS,
221
+ per_device_train_batch_size=PER_DEVICE_TRAIN_BATCH_SIZE,
222
+ gradient_accumulation_steps=GRADIENT_ACCUMULATION_STEPS,
223
+ learning_rate=LEARNING_RATE,
224
+ weight_decay=WEIGHT_DECAY,
225
+ warmup_ratio=WARMUP_RATIO,
226
+ lr_scheduler_type=LR_SCHEDULER,
227
+ max_grad_norm=MAX_GRAD_NORM,
228
+
229
+ # ── Optimizer ────────────────────────────────────────────────────────────
230
+ optim="adamw_torch_fused",
231
+
232
+ # ── Precision ────────────────────────────────────────────────────────────
233
+ bf16=USE_BF16,
234
+ fp16=USE_FP16,
235
+
236
+ # ── Gradient checkpointing ───────────────────────────────────────────────
237
+ gradient_checkpointing=True,
238
+ gradient_checkpointing_kwargs={"use_reentrant": False},
239
+
240
+ # ── Loss ─────────────────────────────────────────────────────────────────
241
+ completion_only_loss=True,
242
+
243
+ # ── Dataset ──────────────────────────────────────────────────────────────
244
+ dataset_text_field="text",
245
+ dataset_num_proc=1, # Must be 1 for IterableDataset (streaming)
246
+
247
+ # ── Logging & checkpointing ──────────────────────────────────────────────
248
+ logging_steps=LOGGING_STEPS,
249
+ save_steps=SAVE_STEPS,
250
+ save_total_limit=SAVE_TOTAL_LIMIT,
251
+ report_to="none", # Swap to "wandb" or "tensorboard" if needed
252
+
253
+ # ── Misc ─────────────────────────────────────────────────────────────────
254
+ seed=SEED,
255
+ remove_unused_columns=True,
256
+ )
257
+
258
+ trainer = SFTTrainer(
259
+ model=model,
260
+ args=sft_config,
261
+ train_dataset=formatted_dataset,
262
+ tokenizer=tokenizer,
263
+ )
264
+
265
+ # =============================================================================
266
+ # 6. TRAIN
267
+ # =============================================================================
268
+ print("\nπŸš€ Starting full fine-tuning …\n")
269
+ trainer.train()
270
+
271
+ # =============================================================================
272
+ # 7. SAVE LOCALLY
273
+ # =============================================================================
274
+ print(f"\nπŸ’Ύ Saving full model + tokenizer to: {OUTPUT_DIR}")
275
+ trainer.save_model(OUTPUT_DIR)
276
+ tokenizer.save_pretrained(OUTPUT_DIR)
277
+ print(" βœ… Local save complete.\n")
278
+
279
+ # =============================================================================
280
+ # 8. PUSH TO HF HUB
281
+ # =============================================================================
282
+ print(f"☁️ Uploading to Hugging Face Hub: {HF_REPO_ID}")
283
+ print(" (This may take a while depending on your upload speed …)\n")
284
+
285
+ try:
286
+ # Push model
287
+ model.push_to_hub(
288
+ HF_REPO_ID,
289
+ token=HF_TOKEN,
290
+ commit_message="Full SFT β€” Qwen3.5-4B-Base on Bc-AI/SFT-Ultra",
291
+ private=True, # Set False if you want a public repo
292
+ )
293
+
294
+ # Push tokenizer
295
+ tokenizer.push_to_hub(
296
+ HF_REPO_ID,
297
+ token=HF_TOKEN,
298
+ commit_message="Add tokenizer",
299
+ )
300
+
301
+ # Push a minimal model card so the repo is well-documented
302
+ api = HfApi()
303
+ model_card = f"""---
304
+ language:
305
+ - en
306
+ license: apache-2.0
307
+ base_model: {MODEL_ID}
308
+ datasets:
309
+ - {DATASET_ID}
310
+ tags:
311
+ - full-fine-tune
312
+ - sft
313
+ - qwen3.5
314
+ ---
315
+
316
+ # Qwen3.5-4B β€” Full SFT
317
+
318
+ - **Base model:** `{MODEL_ID}`
319
+ - **Dataset:** `{DATASET_ID}`
320
+ - **Training type:** Full parameter supervised fine-tuning (no LoRA)
321
+ - **Max sequence length:** {MAX_SEQ_LENGTH}
322
+ - **Epochs:** {NUM_TRAIN_EPOCHS}
323
+ - **Learning rate:** {LEARNING_RATE}
324
+ - **Precision:** {"bf16" if USE_BF16 else "fp16"}
325
+ """
326
+ api.upload_file(
327
+ path_or_fileobj=model_card.encode("utf-8"),
328
+ path_in_repo="README.md",
329
+ repo_id=HF_REPO_ID,
330
+ token=HF_TOKEN,
331
+ commit_message="Add model card",
332
+ )
333
+
334
+ print(f"\nβœ… Model successfully uploaded to: https://huggingface.co/{HF_REPO_ID}")
335
+
336
+ except Exception as e:
337
+ print(f"\n❌ Upload failed: {e}")
338
+ print(f" Your model is still saved locally at: {OUTPUT_DIR}")
339
+ print(" You can retry the upload manually with:")
340
+ print(f" model.push_to_hub('{HF_REPO_ID}')")
341
+ print(f" tokenizer.push_to_hub('{HF_REPO_ID}')")
342
+
343
+ print("\nβœ… All done!")