{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# ✅ Stable Ethical-Hacking QLoRA Fine-Tuning — NO Unsloth, NO Trainer\n",
"\n",
"Backup stable notebook using vanilla `transformers` + `peft` + `bitsandbytes` and a manual PyTorch loop.\n",
"\n",
"## Fix in this revision\n",
"\n",
"The previous manual notebook created a variable-length `labels` column before collation. That caused:\n",
"\n",
"```text\n",
"ValueError: expected sequence of length 2048 at dim 1 (got 232)\n",
"features (`labels` in this case) have excessive nesting\n",
"```\n",
"\n",
"This revision **does not create labels during tokenization**. `DataCollatorForLanguageModeling` now creates and pads labels correctly.\n",
"\n",
"> For lowest VRAM, use the Unsloth LFM2.5 notebook. This is the no-Unsloth fallback.\n"
]
},
{"cell_type":"markdown","metadata":{},"source":["## 1. Install dependencies\n"]},
{"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"]},
{"cell_type":"markdown","metadata":{},"source":["## 2. Optional Hugging Face login\n"]},
{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from huggingface_hub import login\n","# login(token=\"hf_YOUR_WRITE_TOKEN\")\n"]},
{"cell_type":"markdown","metadata":{},"source":["## 3. Configure run\n"]},
{"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"]},
{"cell_type":"markdown","metadata":{},"source":["## 4. Load 4-bit model and attach QLoRA adapter\n"]},
{"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"]},
{"cell_type":"markdown","metadata":{},"source":["## 5. Load and convert dataset\n"]},
{"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"]},
{"cell_type":"markdown","metadata":{},"source":["## 6. Apply chat template and tokenize — fixed labels\n"]},
{"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"]},
{"cell_type":"markdown","metadata":{},"source":["## 7. Build DataLoader\n"]},
{"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"]},
{"cell_type":"markdown","metadata":{},"source":["## 8. Manual training loop\n"]},
{"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"]},
{"cell_type":"markdown","metadata":{},"source":["## 9. Save adapter / optional push\n"]},
{"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"]},
{"cell_type":"markdown","metadata":{},"source":["## 10. Inference sanity check\n"]},
{"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\"\\n{SYSTEM_SAFE}\\n\\n\\n{test_prompt}\\n\\n\\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"]}
],
"metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"name": "python", "version": "3.10"}},
"nbformat": 4,
"nbformat_minor": 5
}