jhsu12's picture
Fix post-training eval crash: use trainer.state.best_metric instead of separate evaluate() call
1365b86 verified
Raw
History Blame Contribute Delete
7.56 kB
"""
Train a single expert adapter for smart contract vulnerability detection.
Usage:
python train_expert.py --expert Reentrancy
python train_expert.py --expert "Access Control"
python train_expert.py --expert "Integer Overflow/Underflow"
python train_expert.py --expert "Timestamp Dependence"
python train_expert.py --expert "Unchecked Low-Level Calls"
Each expert is a LoRA adapter on Qwen2.5-Coder-3B-Instruct, trained to
answer: "Is this contract vulnerable with MY specific vulnerability type?"
Positives: contracts with the expert's vulnerability type
Negatives: safe contracts + contracts with OTHER vulnerability types
"""
import argparse
import os
import torch
from datasets import load_dataset
from peft import LoraConfig
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from trl import SFTConfig, SFTTrainer
from huggingface_hub import HfApi
import trackio
BASE_MODEL = "Qwen/Qwen2.5-Coder-3B-Instruct"
# Expert โ†’ dataset mapping
EXPERT_DATASETS = {
"Reentrancy": "jhsu12/solidity-vuln-expert-reentrancy",
"Access Control": "jhsu12/solidity-vuln-expert-access-control",
"Integer Overflow/Underflow": "jhsu12/solidity-vuln-expert-integer-overflow-underflow",
"Timestamp Dependence": "jhsu12/solidity-vuln-expert-timestamp-dependence",
"Unchecked Low-Level Calls": "jhsu12/solidity-vuln-expert-unchecked-low-level-calls",
}
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--expert", type=str, required=True,
choices=list(EXPERT_DATASETS.keys()),
help="Vulnerability type to train expert for")
parser.add_argument("--output_dir", type=str, default=None)
parser.add_argument("--lora_r", type=int, default=32)
parser.add_argument("--epochs", type=int, default=5)
parser.add_argument("--batch_size", type=int, default=2)
parser.add_argument("--grad_accum", type=int, default=4)
parser.add_argument("--lr", type=float, default=2e-4)
parser.add_argument("--max_seq_len", type=int, default=1536)
parser.add_argument("--push_to_hub", action="store_true", default=True)
return parser.parse_args()
def main():
args = parse_args()
expert_name = args.expert
dataset_id = EXPERT_DATASETS[expert_name]
hub_model_id = f"jhsu12/solidity-vuln-expert-{expert_name.lower().replace(' ', '-').replace('/', '-')}-v1"
output_dir = args.output_dir or f"./expert-{expert_name.lower().replace(' ', '-').replace('/', '-')}"
print(f"=" * 60)
print(f" Training Expert: {expert_name}")
print(f" Base Model: {BASE_MODEL}")
print(f" Dataset: {dataset_id}")
print(f" Hub Model: {hub_model_id}")
print(f"=" * 60)
# GPU config
HAS_BF16 = torch.cuda.is_bf16_supported() if torch.cuda.is_available() else False
GPU_MEM = torch.cuda.get_device_properties(0).total_memory / 1e9 if torch.cuda.is_available() else 0
print(f"\n๐Ÿ–ฅ๏ธ GPU: {torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'CPU'}")
print(f"๐Ÿ’พ VRAM: {GPU_MEM:.1f} GB")
print(f"๐Ÿ”ข BF16: {HAS_BF16}")
compute_dtype = torch.bfloat16 if HAS_BF16 else torch.float16
# Trackio monitoring
trackio.init(
project=f"solidity-expert-{expert_name.lower().replace(' ', '-').replace('/', '-')}",
name=f"{expert_name.lower().replace(' ', '-').replace('/', '-')}-3b-v1",
)
# Load dataset
print(f"\n๐Ÿ“ฆ Loading dataset...")
dataset = load_dataset(dataset_id)
train_dataset = dataset["train"]
eval_dataset = dataset["test"]
print(f" Train: {len(train_dataset)} Eval: {len(eval_dataset)}")
# Load model with 4-bit quantization
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=compute_dtype,
bnb_4bit_use_double_quant=True,
)
print(f"\n๐Ÿค– Loading {BASE_MODEL}...")
model = AutoModelForCausalLM.from_pretrained(
BASE_MODEL,
quantization_config=bnb_config,
device_map="auto",
dtype=compute_dtype,
trust_remote_code=True,
attn_implementation="sdpa",
)
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL, trust_remote_code=True)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
print(" โœ… Model loaded")
# LoRA config
peft_config = LoraConfig(
r=args.lora_r,
lora_alpha=16,
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
target_modules="all-linear",
)
# Training config
training_args = SFTConfig(
output_dir=output_dir,
num_train_epochs=args.epochs,
per_device_train_batch_size=args.batch_size,
per_device_eval_batch_size=1,
gradient_accumulation_steps=args.grad_accum,
eval_accumulation_steps=1,
learning_rate=args.lr,
bf16=HAS_BF16,
fp16=not HAS_BF16,
gradient_checkpointing=True,
gradient_checkpointing_kwargs={"use_reentrant": False},
max_length=args.max_seq_len,
packing=False,
optim="paged_adamw_8bit",
warmup_steps=20,
lr_scheduler_type="cosine",
weight_decay=0.01,
max_grad_norm=0.3,
logging_steps=10,
logging_first_step=True,
logging_strategy="steps",
disable_tqdm=True,
report_to=["trackio"],
save_strategy="epoch",
eval_strategy="epoch",
load_best_model_at_end=True,
metric_for_best_model="eval_loss",
push_to_hub=False, # We push manually at the end
seed=42,
)
# Train
print(f"\n๐Ÿ‹๏ธ Initializing trainer...")
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
processing_class=tokenizer,
peft_config=peft_config,
)
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
total = sum(p.numel() for p in model.parameters())
print(f" Trainable: {trainable:,} ({100*trainable/total:.2f}%)")
print(f"\n๐Ÿš€ Starting training for {expert_name} expert...")
train_result = trainer.train()
print(f"\nโœ… Training complete!")
print(f" Train loss: {train_result.training_loss:.4f}")
# Get best eval loss from training (eval already ran each epoch)
best_eval_loss = trainer.state.best_metric
print(f"\n๐Ÿ“Š Best eval loss (from training): {best_eval_loss:.4f}")
# Save
save_dir = os.path.join(output_dir, "best_model")
print(f"\n๐Ÿ’พ Saving to {save_dir}...")
trainer.save_model(save_dir)
tokenizer.save_pretrained(save_dir)
# Push to hub
if args.push_to_hub:
print(f"\n๐Ÿš€ Pushing to {hub_model_id}...")
api = HfApi()
api.upload_folder(
folder_path=save_dir,
repo_id=hub_model_id,
ignore_patterns=["optimizer*", "scheduler*", "training_args*", "trainer_state*", "rng_state*"],
commit_message=f"Expert adapter for {expert_name} vulnerability detection (3B base)",
)
print(f" โœ… Pushed to https://hf.co/{hub_model_id}")
print(f"\n{'='*60}")
print(f" Expert {expert_name} Complete!")
print(f" Base Model: {BASE_MODEL}")
print(f" Train loss: {train_result.training_loss:.4f}")
print(f" Eval loss: {best_eval_loss:.4f}")
print(f"{'='*60}")
if __name__ == "__main__":
main()