license: apache-2.0
license: apache-2.0 base_model: Qwen/Qwen2.5-3B tags: - qwen2 - java - qlora - domain-adaptation - code - fine-tuned language: - en library_name: transformers
JavaExpert-Qwen2.5-3B
Domain-locked Java QA via QLoRA on consumer hardware.
Fine-tuned from Qwen/Qwen2.5-3B on 7,921 Java QA pairs using QLoRA β trained entirely on a single NVIDIA RTX 5050 (8 GB VRAM), no cloud, no A100s. The model answers Java questions accurately and refuses everything else, without external guardrails.
Evaluation Results
| Metric | Target | Achieved |
|---|---|---|
| Validation Perplexity | < 10 | 2.40 |
| Peak Training VRAM | β€ 8 GB | < 7 GB |
| Inference VRAM | β | < 2 GB |
| Java Correctness | β | 8.5 / 10 |
| Domain Restriction | β | 8.5 / 10 |
| Hallucination Control | β | 8.0 / 10 |
Quick Start
from transformers import AutoModelForCausalLM, AutoTokenizer
model = AutoModelForCausalLM.from_pretrained("Debarun12/JavaExpert-Qwen2.5-3B")
tokenizer = AutoTokenizer.from_pretrained("Debarun12/JavaExpert-Qwen2.5-3B")
messages = [
{"role": "system", "content": "You are JavaExpert, a Java programming assistant."},
{"role": "user", "content": "What is the difference between HashMap and LinkedHashMap in Java?"}
]
input_ids = tokenizer.apply_chat_template(messages, return_tensors="pt")
output = model.generate(input_ids, max_new_tokens=300)
print(tokenizer.decode(output[0], skip_special_tokens=True))
Domain Restriction β What It Does
The model was trained with explicit negative examples (unanswerable/out-of-domain questions), so it refuses non-Java queries without any post-processing filter:
| Query | Response |
|---|---|
| What is the capital of France? | Refuses cleanly |
| Who won the FIFA World Cup? | Refuses cleanly |
| How do I train a neural network in Python? | Partial refusal (known limitation β see below) |
Training Details
Base model: Qwen/Qwen2.5-3B β selected over the 7B (OOM risk on 8 GB) and 1.5B (higher hallucination tendency).
Method: QLoRA β base model quantized to 4-bit NF4 (~1.9 GB), adapters trained in bf16.
Hardware: NVIDIA GeForce RTX 5050, 8 GB VRAM. Peak usage < 7 GB.
Memory Optimisation Decisions
Every config choice was driven by measured VRAM impact:
| Component | Initial | Final | VRAM Saved |
|---|---|---|---|
| LoRA rank | 32 across 7 modules | 16 on q_proj, v_proj | β1.5 GB |
| Batch size | 4 | 1 + grad accum Γ8 | β2.0 GB |
| Optimizer | AdamW | Adafactor | β1.0 GB |
| Compute dtype | fp16 | bf16 | Stable on Blackwell |
Hyperparameters
per_device_train_batch_size = 1
gradient_accumulation_steps = 8 # effective batch = 8
optim = "adafactor"
bf16 = True
learning_rate = 2e-4
num_train_epochs = 3
lora_r = 16
lora_target_modules = ["q_proj", "v_proj"]
Training Loss
| Epoch | Train Loss | Val Loss | Val Perplexity |
|---|---|---|---|
| 1 | 0.8511 | 0.9343 | 2.55 |
| 2 | 0.8367 | 0.8839 | 2.42 |
| 3 | 0.7780 | 0.8772 | 2.40 |
Validation loss decreased monotonically. No overfitting across 3 epochs β the train/val gap remained narrow and stable throughout.
Dataset
Built from scratch β not an off-the-shelf dataset:
- Source: 42,000+ lines of Java documentation, extracted from PDFs
- Pipeline: Custom preprocessing β sliding-window chunking (150 words, 30-word overlap) β LLM-assisted QA generation via
qwen2.5:7b - QA pairs: 7,921 chat-formatted (system + user + assistant)
- Question types: 36% definitional / 32% procedural / 32% reasoning + explicit unanswerable/refusal examples
- Estimated cleanliness: 95%+
Dataset: Debarun12/JavaExpert-Qwen2.5-3B-DATASET
Bugs Found and Fixed During Training
SFTTrainer step-count explosion β packing=True with pre-tokenized input caused 2,139 steps on 90 samples (~24Γ expected). Root cause: SFTTrainer misinterprets packed sequence lengths. Fix: replaced with standard Trainer + DataCollatorForLanguageModeling. Steps normalized to 2,673 across 3 epochs.
PyTorch 2.6 checkpoint incompatibility β weights_only=True default change raised UnpicklingError on resume. Fix: automated removal of rng_state.pth from checkpoint directories before resumption.
Limitations
- Domain-adjacent leakage: Python and general algorithm queries sometimes receive partial responses rather than clean refusals. The boundary between the Java corpus and the 3B's pretraining knowledge on general programming topics is not perfectly sharp.
- Corpus grounding: The model answers Java questions correctly but draws on pretraining generalisation rather than reproducing specific training corpus facts β a fundamental limitation of generative fine-tuning without retrieval.
Recommended next steps: RAG layer over the source corpus for strict factual grounding; DPO with curated refusal preference pairs to sharpen the domain boundary on adjacent technical topics.
Production Deployment
LoRA adapters were merged into base weights via merge_and_unload() β this is a single self-contained checkpoint with no adapter dependency at runtime. Inference runs at under 2 GB VRAM.
Repository
Training notebooks and dataset generation pipeline: GitHub β debarun23
Trained on NVIDIA GeForce RTX 5050 (8 GB VRAM) Β· June 2026 Β· Author: Debarun Das