{
"cells": [
{"cell_type":"markdown","metadata":{},"source":["# ✅ Stable QLoRA Manual Loop — Dataset Options Included\n","\n","This is the **no-Unsloth fallback notebook**. It now includes all dataset choices directly inside the notebook:\n","\n","- `cybersecurity` — Fenrir + Trendyol\n","- `code_corpus` — `krystv/code-corpus-llm-training`\n","- `ultrachat` — `HuggingFaceH4/ultrachat_200k`\n","- `openhermes` — `teknium/OpenHermes-2.5`\n","- `sharegpt_en`, `sharegpt_de`, `sharegpt_hi` — multilingual ShareGPT\n","- `custom_mix` — mix datasets\n","\n","This notebook uses vanilla `transformers` + `peft` + `bitsandbytes` with a manual PyTorch loop. It does **not** use Unsloth or Trainer.\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","\n","# Recommended for free T4\n","MODEL_ID = \"LiquidAI/LFM2.5-1.2B-Instruct\"\n","RUN_NAME = \"lfm25-stable-qlora\"\n","\n","# Stronger but tighter on T4\n","# MODEL_ID = \"Qwen/Qwen3-4B-Instruct-2507\"\n","# RUN_NAME = \"qwen3-4b-stable-qlora\"\n","\n","# Dataset options: cybersecurity | code_corpus | ultrachat | openhermes | sharegpt_en | sharegpt_de | sharegpt_hi | custom_mix\n","DATASET_CHOICE = \"cybersecurity\"\n","\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}-{DATASET_CHOICE}-adapter\"\n","HUB_MODEL_ID = \"your-username/\" + RUN_NAME + \"-\" + DATASET_CHOICE\n","PUSH_TO_HUB = False\n","COMPUTE_DTYPE = torch.float16\n","\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","print(\"DATASET_CHOICE =\", DATASET_CHOICE)\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","\n","bnb_config = BitsAndBytesConfig(\n"," load_in_4bit=True,\n"," bnb_4bit_quant_type=\"nf4\",\n"," bnb_4bit_use_double_quant=True,\n"," bnb_4bit_compute_dtype=COMPUTE_DTYPE,\n",")\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(\n"," MODEL_ID,\n"," quantization_config=bnb_config,\n"," device_map={\"\": 0},\n"," trust_remote_code=True,\n"," low_cpu_mem_usage=True,\n",")\n","model.config.use_cache = False\n","model = prepare_model_for_kbit_training(model, use_gradient_checkpointing=True)\n","lora_config = LoraConfig(\n"," r=LORA_R,\n"," lora_alpha=LORA_ALPHA,\n"," lora_dropout=LORA_DROPOUT,\n"," bias=\"none\",\n"," task_type=TaskType.CAUSAL_LM,\n"," target_modules=TARGET_MODULES,\n"," use_rslora=True,\n",")\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 dataset — all options available here\n"]},
{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from datasets import load_dataset, concatenate_datasets\n","\n","SYSTEM_SAFE = \"You are a cybersecurity education assistant. Provide defensive, ethical, and authorized security guidance only. Refuse harmful or unauthorized requests.\"\n","\n","CUSTOM_DATASETS = [\n"," # (dataset_id, split, n_rows, format_type)\n"," # format_type: cyber_sua | ultrachat | conversations | code\n"," (\"AlicanKiraz0/Cybersecurity-Dataset-Fenrir-v2.1\", \"train\", 5000, \"cyber_sua\"),\n"," (\"Trendyol/Trendyol-Cybersecurity-Instruction-Tuning-Dataset\", \"train\", 5000, \"cyber_sua\"),\n"," (\"teknium/OpenHermes-2.5\", \"train\", 10000, \"conversations\"),\n"," (\"krystv/code-corpus-llm-training\", \"train\", 10000, \"code\"),\n","]\n","\n","def convert_sua(example):\n"," return {\"messages\": [\n"," {\"role\": \"system\", \"content\": example.get(\"system\") or SYSTEM_SAFE},\n"," {\"role\": \"user\", \"content\": example[\"user\"]},\n"," {\"role\": \"assistant\", \"content\": example[\"assistant\"]},\n"," ]}\n","\n","def convert_ultrachat(example):\n"," return {\"messages\": example[\"messages\"]}\n","\n","def convert_conversations(example):\n"," msgs = []\n"," sys_prompt = example.get(\"system_prompt\", \"\") or example.get(\"system\", \"\")\n"," if sys_prompt:\n"," msgs.append({\"role\": \"system\", \"content\": sys_prompt})\n"," for turn in example[\"conversations\"]:\n"," role = \"user\" if turn.get(\"from\") in (\"human\", \"user\") else \"assistant\"\n"," content = turn.get(\"value\", \"\")\n"," if content:\n"," msgs.append({\"role\": role, \"content\": content})\n"," return {\"messages\": msgs}\n","\n","def convert_code(example):\n"," code_text = example.get(\"text\", \"\")\n"," lang = example.get(\"language\", \"\")\n"," repo = example.get(\"repo\", \"unknown\")\n"," prompt = f\"Explain, review, and improve this {lang} code snippet from repo {repo}. Focus on correctness, security, performance, and clarity.\"\n"," return {\"messages\": [\n"," {\"role\": \"system\", \"content\": \"You are a careful coding assistant focused on secure, correct, maintainable code.\"},\n"," {\"role\": \"user\", \"content\": prompt},\n"," {\"role\": \"assistant\", \"content\": code_text},\n"," ]}\n","\n","def apply_converter(ds, fmt):\n"," if fmt == \"cyber_sua\": return ds.map(convert_sua, remove_columns=ds.column_names)\n"," if fmt == \"ultrachat\": return ds.map(convert_ultrachat, remove_columns=ds.column_names)\n"," if fmt == \"conversations\": return ds.map(convert_conversations, remove_columns=ds.column_names)\n"," if fmt == \"code\": return ds.map(convert_code, remove_columns=ds.column_names)\n"," raise ValueError(f\"Unknown format_type: {fmt}\")\n","\n","all_datasets = []\n","\n","if DATASET_CHOICE == \"cybersecurity\":\n"," ds1 = load_dataset(\"AlicanKiraz0/Cybersecurity-Dataset-Fenrir-v2.1\", split=\"train\")\n"," ds2 = load_dataset(\"Trendyol/Trendyol-Cybersecurity-Instruction-Tuning-Dataset\", split=\"train\")\n"," all_datasets = [apply_converter(ds1, \"cyber_sua\"), apply_converter(ds2, \"cyber_sua\")]\n","elif DATASET_CHOICE == \"code_corpus\":\n"," ds = load_dataset(\"krystv/code-corpus-llm-training\", split=\"train\")\n"," all_datasets = [apply_converter(ds, \"code\")]\n","elif DATASET_CHOICE == \"ultrachat\":\n"," ds = load_dataset(\"HuggingFaceH4/ultrachat_200k\", split=\"train_sft\")\n"," all_datasets = [apply_converter(ds, \"ultrachat\")]\n","elif DATASET_CHOICE == \"openhermes\":\n"," ds = load_dataset(\"teknium/OpenHermes-2.5\", split=\"train\")\n"," all_datasets = [apply_converter(ds, \"conversations\")]\n","elif DATASET_CHOICE.startswith(\"sharegpt_\"):\n"," split_map = {\"sharegpt_en\": \"english\", \"sharegpt_de\": \"german_4b_translated\", \"sharegpt_hi\": \"hindi_27b_translated\"}\n"," ds = load_dataset(\"deepmage121/ShareGPT_multilingual\", split=split_map[DATASET_CHOICE])\n"," all_datasets = [apply_converter(ds, \"conversations\")]\n","elif DATASET_CHOICE == \"custom_mix\":\n"," for ds_id, split, n_rows, fmt in CUSTOM_DATASETS:\n"," print(f\"Loading {ds_id} split={split} rows={n_rows} format={fmt}\")\n"," ds = load_dataset(ds_id, split=split)\n"," if n_rows and len(ds) > n_rows:\n"," ds = ds.shuffle(seed=SEED).select(range(n_rows))\n"," all_datasets.append(apply_converter(ds, fmt))\n","else:\n"," raise ValueError(f\"Unknown DATASET_CHOICE: {DATASET_CHOICE}\")\n","\n","dataset = concatenate_datasets(all_datasets) if len(all_datasets) > 1 else all_datasets[0]\n","print(f\"Rows before sampling: {len(dataset):,}\")\n","if SAMPLE_SIZE and len(dataset) > SAMPLE_SIZE:\n"," dataset = dataset.shuffle(seed=SEED).select(range(SAMPLE_SIZE))\n"," print(f\"Rows after sampling: {len(dataset):,}\")\n","print(\"Sample:\")\n","for m in dataset[0][\"messages\"]:\n"," print(m[\"role\"], \":\", m[\"content\"][:160].replace(\"\\n\", \" \"))\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","\n","text_dataset = dataset.map(messages_to_text, remove_columns=[\"messages\"])\n","print(text_dataset[0][\"text\"][:500])\n","\n","def tokenize_fn(batch):\n"," # 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","\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
}