asdf98 commited on
Commit
c1d2a73
·
verified ·
1 Parent(s): 63af8dc

Fix manual QLoRA notebook label collation bug

Browse files
EthicalHacking_Stable_QLoRA_ManualLoop_NO_UNSLOTH.ipynb CHANGED
@@ -6,438 +6,44 @@
6
  "source": [
7
  "# ✅ Stable Ethical-Hacking QLoRA Fine-Tuning — NO Unsloth, NO Trainer\n",
8
  "\n",
9
- "This notebook is rewritten from root because Kaggle kept hitting Unsloth's compiled trainer bug: `AttributeError: 'int' object has no attribute 'mean'`.\n",
10
- "\n",
11
- "## Root fix\n",
12
- "- ❌ No `unsloth`\n",
13
- "- No `SFTTrainer`\n",
14
- "- ❌ No `Trainer.train()`\n",
15
- "- ✅ Vanilla `transformers` + `peft` + `bitsandbytes`\n",
16
- "- Manual PyTorch training loop\n",
17
- "- QLoRA NF4 4-bit training for free Kaggle/Colab T4\n",
18
- "\n",
19
- "Default model is **LiquidAI/LFM2.5-1.2B-Instruct** because it is the most reliable fit for free 16GB T4. You can switch to Qwen3-4B in the config cell if you want more capability but slower training.\n",
20
- "\n",
21
- "> Use only for ethical, defensive, authorized security education and research.\n"
22
- ]
23
- },
24
- {
25
- "cell_type": "markdown",
26
- "metadata": {},
27
- "source": ["## 1. Install dependencies\n"]
28
- },
29
- {
30
- "cell_type": "code",
31
- "execution_count": null,
32
- "metadata": {},
33
- "outputs": [],
34
- "source": [
35
- "!pip install -q -U \"transformers>=4.56.0\" \"peft>=0.18.0\" \"accelerate>=1.0.0\" \"bitsandbytes>=0.45.0\" \"datasets>=3.0.0\" \"huggingface_hub>=0.25.0\" tqdm\n"
36
- ]
37
- },
38
- {
39
- "cell_type": "markdown",
40
- "metadata": {},
41
- "source": ["## 2. Optional Hugging Face login\n"]
42
- },
43
- {
44
- "cell_type": "code",
45
- "execution_count": null,
46
- "metadata": {},
47
- "outputs": [],
48
- "source": [
49
- "from huggingface_hub import login\n",
50
- "# login(token=\"hf_YOUR_WRITE_TOKEN\")\n"
51
- ]
52
- },
53
- {
54
- "cell_type": "markdown",
55
- "metadata": {},
56
- "source": ["## 3. Configure run\n"]
57
- },
58
- {
59
- "cell_type": "code",
60
- "execution_count": null,
61
- "metadata": {},
62
- "outputs": [],
63
- "source": [
64
- "import os, math, random, gc, time\n",
65
- "import torch\n",
66
- "\n",
67
- "# ======================== MODEL CHOICE ========================\n",
68
- "# Recommended for free T4: LiquidAI/LFM2.5-1.2B-Instruct\n",
69
- "MODEL_ID = \"LiquidAI/LFM2.5-1.2B-Instruct\"\n",
70
- "RUN_NAME = \"lfm25-cyber-stable-qlora\"\n",
71
- "\n",
72
- "# Stronger but slower/tighter on T4. Uncomment to use Qwen3-4B:\n",
73
- "# MODEL_ID = \"Qwen/Qwen3-4B-Instruct-2507\"\n",
74
- "# RUN_NAME = \"qwen3-4b-cyber-stable-qlora\"\n",
75
- "\n",
76
- "# ======================== DATA ========================\n",
77
- "DATASET_CHOICE = \"cybersecurity\" # cybersecurity | ultrachat | openhermes | code_corpus\n",
78
- "SAMPLE_SIZE = 30000 # set lower for faster test, e.g. 2000\n",
79
- "MAX_SEQ_LENGTH = 2048 # T4-safe. Try 4096 only for LFM2.5 if VRAM allows\n",
80
- "\n",
81
- "# ======================== QLoRA ========================\n",
82
- "LORA_R = 64\n",
83
- "LORA_ALPHA = 128\n",
84
- "LORA_DROPOUT = 0.05\n",
85
- "TARGET_MODULES = \"all-linear\" # QLoRA-style: target all linear layers\n",
86
- "\n",
87
- "# ======================== TRAINING ========================\n",
88
- "BATCH_SIZE = 1 # micro-batch per GPU. Stable on free T4\n",
89
- "GRAD_ACCUM = 8 # effective batch = 8\n",
90
- "MAX_STEPS = 1000 # raise to 2000-4000 for longer training\n",
91
- "LEARNING_RATE = 2e-4\n",
92
- "WARMUP_STEPS = 50\n",
93
- "WEIGHT_DECAY = 0.01\n",
94
- "LOGGING_STEPS = 10\n",
95
- "SAVE_STEPS = 250\n",
96
- "SEED = 3407\n",
97
- "\n",
98
- "OUTPUT_DIR = f\"./{RUN_NAME}-adapter\"\n",
99
- "HUB_MODEL_ID = \"your-username/\" + RUN_NAME\n",
100
- "PUSH_TO_HUB = False\n",
101
- "\n",
102
- "# T4 supports fp16, not bf16\n",
103
- "COMPUTE_DTYPE = torch.float16\n",
104
- "\n",
105
- "random.seed(SEED)\n",
106
- "torch.manual_seed(SEED)\n",
107
- "if torch.cuda.is_available():\n",
108
- " torch.cuda.manual_seed_all(SEED)\n",
109
- " print(f\"GPU: {torch.cuda.get_device_name(0)}\")\n",
110
- " print(f\"VRAM: {torch.cuda.get_device_properties(0).total_memory/1e9:.2f} GB\")\n",
111
- "else:\n",
112
- " print(\"WARNING: No GPU detected. Enable GPU runtime.\")\n",
113
- "print(\"MODEL_ID =\", MODEL_ID)\n",
114
- "print(\"DATASET_CHOICE =\", DATASET_CHOICE)\n"
115
- ]
116
- },
117
- {
118
- "cell_type": "markdown",
119
- "metadata": {},
120
- "source": ["## 4. Load 4-bit model and attach QLoRA adapter\n"]
121
- },
122
- {
123
- "cell_type": "code",
124
- "execution_count": null,
125
- "metadata": {},
126
- "outputs": [],
127
- "source": [
128
- "from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig\n",
129
- "from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training, TaskType\n",
130
- "\n",
131
- "bnb_config = BitsAndBytesConfig(\n",
132
- " load_in_4bit=True,\n",
133
- " bnb_4bit_quant_type=\"nf4\",\n",
134
- " bnb_4bit_use_double_quant=True,\n",
135
- " bnb_4bit_compute_dtype=COMPUTE_DTYPE,\n",
136
- ")\n",
137
- "\n",
138
- "tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True, use_fast=True)\n",
139
- "if tokenizer.pad_token is None:\n",
140
- " tokenizer.pad_token = tokenizer.eos_token\n",
141
- "tokenizer.padding_side = \"right\"\n",
142
- "\n",
143
- "model = AutoModelForCausalLM.from_pretrained(\n",
144
- " MODEL_ID,\n",
145
- " quantization_config=bnb_config,\n",
146
- " device_map={\"\": 0},\n",
147
- " trust_remote_code=True,\n",
148
- " low_cpu_mem_usage=True,\n",
149
- ")\n",
150
- "\n",
151
- "model.config.use_cache = False\n",
152
- "model = prepare_model_for_kbit_training(model, use_gradient_checkpointing=True)\n",
153
- "\n",
154
- "lora_config = LoraConfig(\n",
155
- " r=LORA_R,\n",
156
- " lora_alpha=LORA_ALPHA,\n",
157
- " lora_dropout=LORA_DROPOUT,\n",
158
- " bias=\"none\",\n",
159
- " task_type=TaskType.CAUSAL_LM,\n",
160
- " target_modules=TARGET_MODULES,\n",
161
- " use_rslora=True,\n",
162
- ")\n",
163
- "\n",
164
- "model = get_peft_model(model, lora_config)\n",
165
- "model.print_trainable_parameters()\n",
166
- "\n",
167
- "if torch.cuda.is_available():\n",
168
- " print(f\"VRAM after model load: {torch.cuda.memory_allocated()/1e9:.2f} GB / {torch.cuda.get_device_properties(0).total_memory/1e9:.2f} GB\")\n"
169
- ]
170
- },
171
- {
172
- "cell_type": "markdown",
173
- "metadata": {},
174
- "source": ["## 5. Load and convert dataset\n"]
175
- },
176
- {
177
- "cell_type": "code",
178
- "execution_count": null,
179
- "metadata": {},
180
- "outputs": [],
181
- "source": [
182
- "from datasets import load_dataset, concatenate_datasets\n",
183
- "\n",
184
- "SYSTEM_SAFE = \"You are a cybersecurity education assistant. Provide defensive, ethical, and authorized security guidance only. Refuse harmful or unauthorized requests.\"\n",
185
- "\n",
186
- "def convert_fenrir(example):\n",
187
- " return {\"messages\": [\n",
188
- " {\"role\": \"system\", \"content\": example.get(\"system\") or SYSTEM_SAFE},\n",
189
- " {\"role\": \"user\", \"content\": example[\"user\"]},\n",
190
- " {\"role\": \"assistant\", \"content\": example[\"assistant\"]},\n",
191
- " ]}\n",
192
- "\n",
193
- "def convert_trendyol(example):\n",
194
- " return {\"messages\": [\n",
195
- " {\"role\": \"system\", \"content\": example.get(\"system\") or SYSTEM_SAFE},\n",
196
- " {\"role\": \"user\", \"content\": example[\"user\"]},\n",
197
- " {\"role\": \"assistant\", \"content\": example[\"assistant\"]},\n",
198
- " ]}\n",
199
- "\n",
200
- "def convert_ultrachat(example):\n",
201
- " return {\"messages\": example[\"messages\"]}\n",
202
- "\n",
203
- "def convert_conversations(example):\n",
204
- " msgs = []\n",
205
- " sys = example.get(\"system_prompt\", \"\") or example.get(\"system\", \"\")\n",
206
- " if sys:\n",
207
- " msgs.append({\"role\": \"system\", \"content\": sys})\n",
208
- " for turn in example[\"conversations\"]:\n",
209
- " role = \"user\" if turn.get(\"from\") in (\"human\", \"user\") else \"assistant\"\n",
210
- " msgs.append({\"role\": role, \"content\": turn.get(\"value\", \"\")})\n",
211
- " return {\"messages\": msgs}\n",
212
- "\n",
213
- "def convert_code(example):\n",
214
- " code = example[\"text\"]\n",
215
- " lang = example.get(\"language\", \"\")\n",
216
- " prompt = f\"Explain and improve this {lang} code snippet. Focus on correctness, safety, and clarity.\"\n",
217
- " return {\"messages\": [\n",
218
- " {\"role\": \"user\", \"content\": prompt},\n",
219
- " {\"role\": \"assistant\", \"content\": code},\n",
220
- " ]}\n",
221
- "\n",
222
- "if DATASET_CHOICE == \"cybersecurity\":\n",
223
- " ds1 = load_dataset(\"AlicanKiraz0/Cybersecurity-Dataset-Fenrir-v2.1\", split=\"train\")\n",
224
- " ds1 = ds1.map(convert_fenrir, remove_columns=ds1.column_names)\n",
225
- " ds2 = load_dataset(\"Trendyol/Trendyol-Cybersecurity-Instruction-Tuning-Dataset\", split=\"train\")\n",
226
- " ds2 = ds2.map(convert_trendyol, remove_columns=ds2.column_names)\n",
227
- " dataset = concatenate_datasets([ds1, ds2])\n",
228
- "elif DATASET_CHOICE == \"ultrachat\":\n",
229
- " dataset = load_dataset(\"HuggingFaceH4/ultrachat_200k\", split=\"train_sft\")\n",
230
- " dataset = dataset.map(convert_ultrachat, remove_columns=dataset.column_names)\n",
231
- "elif DATASET_CHOICE == \"openhermes\":\n",
232
- " dataset = load_dataset(\"teknium/OpenHermes-2.5\", split=\"train\")\n",
233
- " dataset = dataset.map(convert_conversations, remove_columns=dataset.column_names)\n",
234
- "elif DATASET_CHOICE == \"code_corpus\":\n",
235
- " dataset = load_dataset(\"krystv/code-corpus-llm-training\", split=\"train\")\n",
236
- " dataset = dataset.map(convert_code, remove_columns=dataset.column_names)\n",
237
- "else:\n",
238
- " raise ValueError(DATASET_CHOICE)\n",
239
- "\n",
240
- "print(f\"Loaded rows: {len(dataset):,}\")\n",
241
- "if SAMPLE_SIZE and len(dataset) > SAMPLE_SIZE:\n",
242
- " dataset = dataset.shuffle(seed=SEED).select(range(SAMPLE_SIZE))\n",
243
- " print(f\"Subsampled rows: {len(dataset):,}\")\n",
244
- "\n",
245
- "sample = dataset[0][\"messages\"]\n",
246
- "print(\"Sample roles:\", [m[\"role\"] for m in sample])\n",
247
- "for m in sample[:3]:\n",
248
- " print(m[\"role\"], \":\", m[\"content\"][:140].replace(\"\\n\", \" \"))\n"
249
- ]
250
- },
251
- {
252
- "cell_type": "markdown",
253
- "metadata": {},
254
- "source": ["## 6. Apply chat template and tokenize\n"]
255
- },
256
- {
257
- "cell_type": "code",
258
- "execution_count": null,
259
- "metadata": {},
260
- "outputs": [],
261
- "source": [
262
- "def messages_to_text(example):\n",
263
- " try:\n",
264
- " text = tokenizer.apply_chat_template(example[\"messages\"], tokenize=False, add_generation_prompt=False)\n",
265
- " except Exception:\n",
266
- " parts = []\n",
267
- " for m in example[\"messages\"]:\n",
268
- " parts.append(f\"<{m['role']}>\\n{m['content']}\\n</{m['role']}>\")\n",
269
- " text = \"\\n\".join(parts) + tokenizer.eos_token\n",
270
- " return {\"text\": text}\n",
271
- "\n",
272
- "text_dataset = dataset.map(messages_to_text, remove_columns=[\"messages\"])\n",
273
- "print(text_dataset[0][\"text\"][:500])\n",
274
- "\n",
275
- "def tokenize_fn(batch):\n",
276
- " out = tokenizer(batch[\"text\"], truncation=True, max_length=MAX_SEQ_LENGTH, padding=False, add_special_tokens=False)\n",
277
- " out[\"labels\"] = [ids.copy() for ids in out[\"input_ids\"]]\n",
278
- " return out\n",
279
- "\n",
280
- "tokenized = text_dataset.map(tokenize_fn, batched=True, remove_columns=[\"text\"], num_proc=2, desc=\"Tokenizing\")\n",
281
- "tokenized = tokenized.filter(lambda x: len(x[\"input_ids\"]) > 8)\n",
282
- "print(f\"Tokenized rows: {len(tokenized):,}\")\n",
283
- "print(\"First length:\", len(tokenized[0][\"input_ids\"]))\n"
284
- ]
285
- },
286
- {
287
- "cell_type": "markdown",
288
- "metadata": {},
289
- "source": ["## 7. Build DataLoader\n"]
290
- },
291
- {
292
- "cell_type": "code",
293
- "execution_count": null,
294
- "metadata": {},
295
- "outputs": [],
296
- "source": [
297
- "from torch.utils.data import DataLoader\n",
298
- "from transformers import DataCollatorForLanguageModeling\n",
299
- "\n",
300
- "collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False, pad_to_multiple_of=8)\n",
301
- "tokenized.set_format(type=\"torch\", columns=[\"input_ids\", \"attention_mask\", \"labels\"])\n",
302
- "train_loader = DataLoader(tokenized, batch_size=BATCH_SIZE, shuffle=True, collate_fn=collator, num_workers=0)\n",
303
- "print(\"Batches per epoch:\", len(train_loader))\n",
304
- "print(\"Effective batch size:\", BATCH_SIZE * GRAD_ACCUM)\n"
305
- ]
306
- },
307
- {
308
- "cell_type": "markdown",
309
- "metadata": {},
310
- "source": ["## 8. Manual training loop — no Trainer, no Unsloth\n"]
311
- },
312
- {
313
- "cell_type": "code",
314
- "execution_count": null,
315
- "metadata": {},
316
- "outputs": [],
317
- "source": [
318
- "import bitsandbytes as bnb\n",
319
- "from transformers import get_cosine_schedule_with_warmup\n",
320
- "from tqdm.auto import tqdm\n",
321
- "\n",
322
- "trainable_params = [p for p in model.parameters() if p.requires_grad]\n",
323
- "optimizer = bnb.optim.AdamW8bit(trainable_params, lr=LEARNING_RATE, weight_decay=WEIGHT_DECAY)\n",
324
- "scheduler = get_cosine_schedule_with_warmup(optimizer, num_warmup_steps=WARMUP_STEPS, num_training_steps=MAX_STEPS)\n",
325
- "\n",
326
- "model.train()\n",
327
- "global_step = 0\n",
328
- "micro_step = 0\n",
329
- "running_loss = 0.0\n",
330
- "start_time = time.time()\n",
331
- "progress = tqdm(total=MAX_STEPS, desc=\"Training\", dynamic_ncols=True)\n",
332
- "optimizer.zero_grad(set_to_none=True)\n",
333
- "\n",
334
- "while global_step < MAX_STEPS:\n",
335
- " for batch in train_loader:\n",
336
- " batch = {k: v.to(model.device) for k, v in batch.items()}\n",
337
- " outputs = model(**batch)\n",
338
- " loss = outputs.loss / GRAD_ACCUM\n",
339
- " loss.backward()\n",
340
- " running_loss += loss.item() * GRAD_ACCUM\n",
341
- " micro_step += 1\n",
342
- "\n",
343
- " if micro_step % GRAD_ACCUM == 0:\n",
344
- " torch.nn.utils.clip_grad_norm_(trainable_params, 1.0)\n",
345
- " optimizer.step()\n",
346
- " scheduler.step()\n",
347
- " optimizer.zero_grad(set_to_none=True)\n",
348
- " global_step += 1\n",
349
- " progress.update(1)\n",
350
- "\n",
351
- " if global_step % LOGGING_STEPS == 0:\n",
352
- " avg_loss = running_loss / LOGGING_STEPS\n",
353
- " running_loss = 0.0\n",
354
- " lr = scheduler.get_last_lr()[0]\n",
355
- " elapsed = (time.time() - start_time) / 60\n",
356
- " if torch.cuda.is_available():\n",
357
- " vram = torch.cuda.memory_allocated() / 1e9\n",
358
- " total = torch.cuda.get_device_properties(0).total_memory / 1e9\n",
359
- " print(f\"step={global_step} loss={avg_loss:.4f} lr={lr:.2e} vram={vram:.2f}/{total:.2f}GB elapsed={elapsed:.1f}m\")\n",
360
- " else:\n",
361
- " print(f\"step={global_step} loss={avg_loss:.4f} lr={lr:.2e} elapsed={elapsed:.1f}m\")\n",
362
- "\n",
363
- " if global_step % SAVE_STEPS == 0:\n",
364
- " ckpt = f\"{OUTPUT_DIR}/checkpoint-{global_step}\"\n",
365
- " model.save_pretrained(ckpt)\n",
366
- " tokenizer.save_pretrained(ckpt)\n",
367
- " print(f\"Saved checkpoint: {ckpt}\")\n",
368
- "\n",
369
- " if global_step >= MAX_STEPS:\n",
370
- " break\n",
371
- "\n",
372
- "progress.close()\n",
373
- "print(\"Training complete\")\n"
374
- ]
375
- },
376
- {
377
- "cell_type": "markdown",
378
- "metadata": {},
379
- "source": ["## 9. Save adapter / optional push\n"]
380
- },
381
- {
382
- "cell_type": "code",
383
- "execution_count": null,
384
- "metadata": {},
385
- "outputs": [],
386
- "source": [
387
- "model.save_pretrained(OUTPUT_DIR)\n",
388
- "tokenizer.save_pretrained(OUTPUT_DIR)\n",
389
- "print(f\"Saved final LoRA adapter to: {OUTPUT_DIR}\")\n",
390
- "\n",
391
- "if PUSH_TO_HUB:\n",
392
- " model.push_to_hub(HUB_MODEL_ID)\n",
393
- " tokenizer.push_to_hub(HUB_MODEL_ID)\n",
394
- " print(f\"Pushed to: https://huggingface.co/{HUB_MODEL_ID}\")\n"
395
- ]
396
- },
397
- {
398
- "cell_type": "markdown",
399
- "metadata": {},
400
- "source": ["## 10. Inference sanity check\n"]
401
- },
402
- {
403
- "cell_type": "code",
404
- "execution_count": null,
405
- "metadata": {},
406
- "outputs": [],
407
- "source": [
408
- "model.eval()\n",
409
- "test_prompt = \"Explain how parameterized queries prevent SQL injection. Give a safe Python example.\"\n",
410
- "messages = [{\"role\": \"system\", \"content\": SYSTEM_SAFE}, {\"role\": \"user\", \"content\": test_prompt}]\n",
411
- "try:\n",
412
- " prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)\n",
413
- "except Exception:\n",
414
- " prompt = f\"<system>\\n{SYSTEM_SAFE}\\n</system>\\n<user>\\n{test_prompt}\\n</user>\\n<assistant>\\n\"\n",
415
- "inputs = tokenizer(prompt, return_tensors=\"pt\").to(model.device)\n",
416
- "with torch.no_grad():\n",
417
- " out = model.generate(**inputs, max_new_tokens=384, do_sample=True, temperature=0.7, top_p=0.9, pad_token_id=tokenizer.pad_token_id, eos_token_id=tokenizer.eos_token_id)\n",
418
- "print(tokenizer.decode(out[0][inputs[\"input_ids\"].shape[1]:], skip_special_tokens=True))\n"
419
- ]
420
- },
421
- {
422
- "cell_type": "markdown",
423
- "metadata": {},
424
- "source": [
425
- "## Troubleshooting\n",
426
- "\n",
427
- "If you OOM on T4, use this order:\n",
428
- "1. Keep `BATCH_SIZE=1`; increase/decrease `GRAD_ACCUM` only.\n",
429
- "2. Lower `MAX_SEQ_LENGTH` to 1024.\n",
430
- "3. Lower `LORA_R` to 32.\n",
431
- "4. Lower `SAMPLE_SIZE` and `MAX_STEPS` for a fast smoke test.\n",
432
- "\n",
433
- "The previous `int.mean()` error cannot happen here because this notebook never calls Unsloth or `Trainer.train()`.\n"
434
- ]
435
- }
436
  ],
437
- "metadata": {
438
- "kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"},
439
- "language_info": {"name": "python", "version": "3.10"}
440
- },
441
  "nbformat": 4,
442
  "nbformat_minor": 5
443
  }
 
6
  "source": [
7
  "# ✅ Stable Ethical-Hacking QLoRA Fine-Tuning — NO Unsloth, NO Trainer\n",
8
  "\n",
9
+ "Backup stable notebook using vanilla `transformers` + `peft` + `bitsandbytes` and a manual PyTorch loop.\n",
10
+ "\n",
11
+ "## Fix in this revision\n",
12
+ "\n",
13
+ "The previous manual notebook created a variable-length `labels` column before collation. That caused:\n",
14
+ "\n",
15
+ "```text\n",
16
+ "ValueError: expected sequence of length 2048 at dim 1 (got 232)\n",
17
+ "features (`labels` in this case) have excessive nesting\n",
18
+ "```\n",
19
+ "\n",
20
+ "This revision **does not create labels during tokenization**. `DataCollatorForLanguageModeling` now creates and pads labels correctly.\n",
21
+ "\n",
22
+ "> For lowest VRAM, use the Unsloth LFM2.5 notebook. This is the no-Unsloth fallback.\n"
23
+ ]
24
+ },
25
+ {"cell_type":"markdown","metadata":{},"source":["## 1. Install dependencies\n"]},
26
+ {"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["!pip install -q -U \"transformers>=4.56.0\" \"peft>=0.18.0\" \"accelerate>=1.0.0\" \"bitsandbytes>=0.45.0\" \"datasets>=3.0.0\" \"huggingface_hub>=0.25.0\" tqdm\n"]},
27
+ {"cell_type":"markdown","metadata":{},"source":["## 2. Optional Hugging Face login\n"]},
28
+ {"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from huggingface_hub import login\n","# login(token=\"hf_YOUR_WRITE_TOKEN\")\n"]},
29
+ {"cell_type":"markdown","metadata":{},"source":["## 3. Configure run\n"]},
30
+ {"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["import os, math, random, gc, time\n","import torch\n","MODEL_ID = \"LiquidAI/LFM2.5-1.2B-Instruct\"\n","RUN_NAME = \"lfm25-cyber-stable-qlora\"\n","# MODEL_ID = \"Qwen/Qwen3-4B-Instruct-2507\"\n","# RUN_NAME = \"qwen3-4b-cyber-stable-qlora\"\n","DATASET_CHOICE = \"cybersecurity\"\n","SAMPLE_SIZE = 30000\n","MAX_SEQ_LENGTH = 2048\n","LORA_R = 64\n","LORA_ALPHA = 128\n","LORA_DROPOUT = 0.05\n","TARGET_MODULES = \"all-linear\"\n","BATCH_SIZE = 1\n","GRAD_ACCUM = 8\n","MAX_STEPS = 1000\n","LEARNING_RATE = 2e-4\n","WARMUP_STEPS = 50\n","WEIGHT_DECAY = 0.01\n","LOGGING_STEPS = 10\n","SAVE_STEPS = 250\n","SEED = 3407\n","OUTPUT_DIR = f\"./{RUN_NAME}-adapter\"\n","HUB_MODEL_ID = \"your-username/\" + RUN_NAME\n","PUSH_TO_HUB = False\n","COMPUTE_DTYPE = torch.float16\n","random.seed(SEED)\n","torch.manual_seed(SEED)\n","if torch.cuda.is_available():\n"," torch.cuda.manual_seed_all(SEED)\n"," print(f\"GPU: {torch.cuda.get_device_name(0)}\")\n"," print(f\"VRAM: {torch.cuda.get_device_properties(0).total_memory/1e9:.2f} GB\")\n","else:\n"," print(\"WARNING: No GPU detected. Enable GPU runtime.\")\n","print(\"MODEL_ID =\", MODEL_ID)\n"]},
31
+ {"cell_type":"markdown","metadata":{},"source":["## 4. Load 4-bit model and attach QLoRA adapter\n"]},
32
+ {"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig\n","from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training, TaskType\n","bnb_config = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type=\"nf4\", bnb_4bit_use_double_quant=True, bnb_4bit_compute_dtype=COMPUTE_DTYPE)\n","tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True, use_fast=True)\n","if tokenizer.pad_token is None:\n"," tokenizer.pad_token = tokenizer.eos_token\n","tokenizer.padding_side = \"right\"\n","model = AutoModelForCausalLM.from_pretrained(MODEL_ID, quantization_config=bnb_config, device_map={\"\": 0}, trust_remote_code=True, low_cpu_mem_usage=True)\n","model.config.use_cache = False\n","model = prepare_model_for_kbit_training(model, use_gradient_checkpointing=True)\n","lora_config = LoraConfig(r=LORA_R, lora_alpha=LORA_ALPHA, lora_dropout=LORA_DROPOUT, bias=\"none\", task_type=TaskType.CAUSAL_LM, target_modules=TARGET_MODULES, use_rslora=True)\n","model = get_peft_model(model, lora_config)\n","model.print_trainable_parameters()\n","if torch.cuda.is_available():\n"," print(f\"VRAM after model load: {torch.cuda.memory_allocated()/1e9:.2f} GB / {torch.cuda.get_device_properties(0).total_memory/1e9:.2f} GB\")\n"]},
33
+ {"cell_type":"markdown","metadata":{},"source":["## 5. Load and convert dataset\n"]},
34
+ {"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from datasets import load_dataset, concatenate_datasets\n","SYSTEM_SAFE = \"You are a cybersecurity education assistant. Provide defensive, ethical, and authorized security guidance only. Refuse harmful or unauthorized requests.\"\n","def convert_sua(example):\n"," return {\"messages\": [{\"role\": \"system\", \"content\": example.get(\"system\") or SYSTEM_SAFE}, {\"role\": \"user\", \"content\": example[\"user\"]}, {\"role\": \"assistant\", \"content\": example[\"assistant\"]}]}\n","if DATASET_CHOICE == \"cybersecurity\":\n"," ds1 = load_dataset(\"AlicanKiraz0/Cybersecurity-Dataset-Fenrir-v2.1\", split=\"train\")\n"," ds1 = ds1.map(convert_sua, remove_columns=ds1.column_names)\n"," ds2 = load_dataset(\"Trendyol/Trendyol-Cybersecurity-Instruction-Tuning-Dataset\", split=\"train\")\n"," ds2 = ds2.map(convert_sua, remove_columns=ds2.column_names)\n"," dataset = concatenate_datasets([ds1, ds2])\n","else:\n"," raise ValueError(\"This compact fallback notebook currently defaults to cybersecurity only.\")\n","print(f\"Loaded rows: {len(dataset):,}\")\n","if SAMPLE_SIZE and len(dataset) > SAMPLE_SIZE:\n"," dataset = dataset.shuffle(seed=SEED).select(range(SAMPLE_SIZE))\n"," print(f\"Subsampled rows: {len(dataset):,}\")\n","print(dataset[0][\"messages\"])\n"]},
35
+ {"cell_type":"markdown","metadata":{},"source":["## 6. Apply chat template and tokenize — fixed labels\n"]},
36
+ {"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def messages_to_text(example):\n"," try:\n"," text = tokenizer.apply_chat_template(example[\"messages\"], tokenize=False, add_generation_prompt=False)\n"," except Exception:\n"," parts = [f\"<{m['role']}>\\n{m['content']}\\n</{m['role']}>\" for m in example[\"messages\"]]\n"," text = \"\\n\".join(parts) + tokenizer.eos_token\n"," return {\"text\": text}\n","text_dataset = dataset.map(messages_to_text, remove_columns=[\"messages\"])\n","print(text_dataset[0][\"text\"][:500])\n","def tokenize_fn(batch):\n"," # IMPORTANT: Do NOT create labels here. The data collator creates/pads labels safely.\n"," return tokenizer(batch[\"text\"], truncation=True, max_length=MAX_SEQ_LENGTH, padding=False, add_special_tokens=False)\n","tokenized = text_dataset.map(tokenize_fn, batched=True, remove_columns=[\"text\"], num_proc=2, desc=\"Tokenizing\")\n","tokenized = tokenized.filter(lambda x: len(x[\"input_ids\"]) > 8)\n","print(f\"Tokenized rows: {len(tokenized):,}\")\n","print(\"First length:\", len(tokenized[0][\"input_ids\"]))\n"]},
37
+ {"cell_type":"markdown","metadata":{},"source":["## 7. Build DataLoader\n"]},
38
+ {"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from torch.utils.data import DataLoader\n","from transformers import DataCollatorForLanguageModeling\n","collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False, pad_to_multiple_of=8)\n","tokenized.set_format(type=\"torch\", columns=[\"input_ids\", \"attention_mask\"])\n","train_loader = DataLoader(tokenized, batch_size=BATCH_SIZE, shuffle=True, collate_fn=collator, num_workers=0)\n","print(\"Batches per epoch:\", len(train_loader))\n","print(\"Effective batch size:\", BATCH_SIZE * GRAD_ACCUM)\n"]},
39
+ {"cell_type":"markdown","metadata":{},"source":["## 8. Manual training loop\n"]},
40
+ {"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["import bitsandbytes as bnb\n","from transformers import get_cosine_schedule_with_warmup\n","from tqdm.auto import tqdm\n","trainable_params = [p for p in model.parameters() if p.requires_grad]\n","optimizer = bnb.optim.AdamW8bit(trainable_params, lr=LEARNING_RATE, weight_decay=WEIGHT_DECAY)\n","scheduler = get_cosine_schedule_with_warmup(optimizer, num_warmup_steps=WARMUP_STEPS, num_training_steps=MAX_STEPS)\n","model.train()\n","global_step = 0\n","micro_step = 0\n","running_loss = 0.0\n","start_time = time.time()\n","progress = tqdm(total=MAX_STEPS, desc=\"Training\", dynamic_ncols=True)\n","optimizer.zero_grad(set_to_none=True)\n","while global_step < MAX_STEPS:\n"," for batch in train_loader:\n"," batch = {k: v.to(model.device) for k, v in batch.items()}\n"," outputs = model(**batch)\n"," loss = outputs.loss / GRAD_ACCUM\n"," loss.backward()\n"," running_loss += loss.item() * GRAD_ACCUM\n"," micro_step += 1\n"," if micro_step % GRAD_ACCUM == 0:\n"," torch.nn.utils.clip_grad_norm_(trainable_params, 1.0)\n"," optimizer.step(); scheduler.step(); optimizer.zero_grad(set_to_none=True)\n"," global_step += 1; progress.update(1)\n"," if global_step % LOGGING_STEPS == 0:\n"," avg_loss = running_loss / LOGGING_STEPS; running_loss = 0.0\n"," lr = scheduler.get_last_lr()[0]\n"," elapsed = (time.time() - start_time) / 60\n"," if torch.cuda.is_available():\n"," vram = torch.cuda.memory_allocated() / 1e9; total = torch.cuda.get_device_properties(0).total_memory / 1e9\n"," print(f\"step={global_step} loss={avg_loss:.4f} lr={lr:.2e} vram={vram:.2f}/{total:.2f}GB elapsed={elapsed:.1f}m\")\n"," else:\n"," print(f\"step={global_step} loss={avg_loss:.4f} lr={lr:.2e} elapsed={elapsed:.1f}m\")\n"," if global_step % SAVE_STEPS == 0:\n"," ckpt = f\"{OUTPUT_DIR}/checkpoint-{global_step}\"\n"," model.save_pretrained(ckpt); tokenizer.save_pretrained(ckpt)\n"," print(f\"Saved checkpoint: {ckpt}\")\n"," if global_step >= MAX_STEPS: break\n","progress.close()\n","print(\"Training complete\")\n"]},
41
+ {"cell_type":"markdown","metadata":{},"source":["## 9. Save adapter / optional push\n"]},
42
+ {"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["model.save_pretrained(OUTPUT_DIR)\n","tokenizer.save_pretrained(OUTPUT_DIR)\n","print(f\"Saved final LoRA adapter to: {OUTPUT_DIR}\")\n","if PUSH_TO_HUB:\n"," model.push_to_hub(HUB_MODEL_ID); tokenizer.push_to_hub(HUB_MODEL_ID)\n"]},
43
+ {"cell_type":"markdown","metadata":{},"source":["## 10. Inference sanity check\n"]},
44
+ {"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["model.eval()\n","test_prompt = \"Explain how parameterized queries prevent SQL injection. Give a safe Python example.\"\n","messages = [{\"role\": \"system\", \"content\": SYSTEM_SAFE}, {\"role\": \"user\", \"content\": test_prompt}]\n","try:\n"," prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)\n","except Exception:\n"," prompt = f\"<system>\\n{SYSTEM_SAFE}\\n</system>\\n<user>\\n{test_prompt}\\n</user>\\n<assistant>\\n\"\n","inputs = tokenizer(prompt, return_tensors=\"pt\").to(model.device)\n","with torch.no_grad():\n"," out = model.generate(**inputs, max_new_tokens=384, do_sample=True, temperature=0.7, top_p=0.9, pad_token_id=tokenizer.pad_token_id, eos_token_id=tokenizer.eos_token_id)\n","print(tokenizer.decode(out[0][inputs[\"input_ids\"].shape[1]:], skip_special_tokens=True))\n"]}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  ],
46
+ "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"name": "python", "version": "3.10"}},
 
 
 
47
  "nbformat": 4,
48
  "nbformat_minor": 5
49
  }