kaustubhkar
Auto-commit: 2026-05-01 03:17:11
00c89e5
Raw
History Blame Contribute Delete
11.3 kB
#!/usr/bin/env python3
"""
train_colab.py β€” AWS Cloud Config Auditor
GRPO training tuned to reproduce Run 2 (loss 0.6124, no NaN grads, 100 steps)
"""
import os, json, re, requests, torch
os.environ["UNSLOTH_RETURN_LOGITS"] = "1"
os.environ["UNSLOTH_CACHE_DIR"] = "/tmp/unsloth_cache"
import unsloth
from unsloth import FastLanguageModel, PatchFastRL
PatchFastRL("GRPO", FastLanguageModel)
from trl import GRPOConfig, GRPOTrainer
from datasets import Dataset
ENV_BASE_URL = os.getenv("ENV_BASE_URL", "https://kkaustav-cloud-config-auditor.hf.space")
MODEL_NAME = os.getenv("MODEL_NAME", "unsloth/Qwen2.5-3B-Instruct-bnb-4bit")
OUTPUT_DIR = os.getenv("OUTPUT_DIR", "/content/cloud-config-auditor/grpo_output")
HF_REPO = os.getenv("HF_REPO", "kkaustav/aws-security-auditor-lora")
HF_TOKEN = os.getenv("HF_TOKEN", "")
MAX_SEQ_LEN = 2048
LORA_RANK = 16
CURRICULUM = ["easy_security_group", "medium_s3_policy", "hard_iam_vpc"]
# ── Env Client ─────────────────────────────────────────────────────────────────
def env_reset(task):
r = requests.post(f"{ENV_BASE_URL}/reset?task={task}", timeout=30)
r.raise_for_status(); return r.json()
def env_step(findings, severity, recommendations, config_patch):
r = requests.post(f"{ENV_BASE_URL}/step", json={
"findings": findings, "severity": severity,
"recommendations": recommendations, "config_patch": config_patch
}, timeout=30)
r.raise_for_status(); return r.json()
def env_health():
try: return requests.get(f"{ENV_BASE_URL}/health", timeout=10).status_code == 200
except: return False
# ── Prompt ─────────────────────────────────────────────────────────────────────
SYSTEM_PROMPT = """You are an expert AWS cloud security auditor.
Return your audit as valid JSON ONLY (no markdown, no code blocks):
{
"findings": ["finding 1", "finding 2"],
"severity": ["CRITICAL", "HIGH"],
"recommendations": ["fix step 1", "fix step 2"],
"config_patch": {"key": "suggested_value"}
}
Severity levels: CRITICAL, HIGH, MEDIUM, LOW. Be specific β€” generic findings score zero."""
def build_prompt(config, desc):
return (
f"<|im_start|>system\n{SYSTEM_PROMPT}<|im_end|>\n"
f"<|im_start|>user\nTask: {desc}\n\nConfiguration:\n{config}<|im_end|>\n"
f"<|im_start|>assistant\n"
)
# ── Parser ─────────────────────────────────────────────────────────────────────
def parse_response(text):
try: data = json.loads(text.strip())
except:
m = re.search(r'\{.*\}', text, re.DOTALL)
try: data = json.loads(m.group()) if m else {}
except: data = {}
raw_findings = data.get("findings", [])
findings = []
if isinstance(raw_findings, list):
for f in raw_findings:
if isinstance(f, dict): findings.append(f.get("description", f.get("rule", str(f))))
elif isinstance(f, str): findings.append(f)
raw_sev = data.get("severity", [])
if isinstance(raw_sev, str): severity = [raw_sev]
elif isinstance(raw_sev, list): severity = [s.get("severity", s) if isinstance(s, dict) else s for s in raw_sev]
else: severity = []
return {
"findings": findings,
"severity": severity,
"recommendations": data.get("recommendations", []) if isinstance(data.get("recommendations"), list) else [],
"config_patch": data.get("config_patch", {}) if isinstance(data.get("config_patch"), dict) else {},
}
# ── Reward ─────────────────────────────────────────────────────────────────────
def reward_fn(completions, prompts=None, **kwargs):
rewards, dataset_ref = [], kwargs.get("dataset_ref", [])
for i, completion in enumerate(completions):
if len(completion.strip()) < 30: rewards.append(0.0); continue
parsed = parse_response(completion)
if not parsed["findings"]: rewards.append(0.01); continue
try:
task = dataset_ref[i % len(dataset_ref)]["task_name"] if dataset_ref else CURRICULUM[0]
env_reset(task)
result = env_step(parsed["findings"], parsed["severity"],
parsed["recommendations"], parsed["config_patch"])
base = float(result.get("reward", 0.0))
bonus = sum(0.02 for k in ["findings","severity","recommendations","config_patch"] if parsed[k])
if all(s in {"CRITICAL","HIGH","MEDIUM","LOW"} for s in parsed["severity"]): bonus += 0.02
# Penalise vague responses
vague = ["review your settings", "consult aws docs", "see documentation"]
penalty = 0.05 if any(v in " ".join(parsed["findings"]).lower() for v in vague) else 0.0
rewards.append(min(0.99, max(0.0, base + bonus - penalty)))
except Exception as e:
print(f"[reward] {e}"); rewards.append(0.0)
return rewards
# ── Dataset ────────────────────────────────────────────────────────────────────
def build_dataset(n=10): # ← Run 2 used 10 per task
records = []
for task in CURRICULUM:
for _ in range(n):
try:
obs = env_reset(task).get("observation", {})
records.append({
"prompt": build_prompt(obs.get("config",""), obs.get("task_description", task)),
"task_name": task,
})
except Exception as e: print(f"[dataset] {task}: {e}")
return Dataset.from_list(records)
# ── Evaluate ───────────────────────────────────────────────────────────────────
def evaluate(model, tokenizer, label, n=3):
print(f"\n{'='*55}\n{label}\n{'='*55}")
scores = {}
for task in CURRICULUM:
task_scores = []
for i in range(n):
try:
obs = env_reset(task).get("observation", {})
inputs = tokenizer(
build_prompt(obs.get("config",""), obs.get("task_description", task)),
return_tensors="pt"
).to("cuda")
with torch.no_grad():
out = model.generate(**inputs, max_new_tokens=384, temperature=0.7, do_sample=True)
response = tokenizer.decode(out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)
parsed = parse_response(response)
env_reset(task)
score = float(env_step(parsed["findings"], parsed["severity"],
parsed["recommendations"], parsed["config_patch"]).get("reward", 0.0))
task_scores.append(score)
print(f" [{task}] run {i+1}: {score:.4f}")
except Exception as e:
print(f" [{task}] run {i+1}: error β€” {e}"); task_scores.append(0.0)
scores[task] = round(sum(task_scores)/len(task_scores), 4)
print(f" [{task}] avg = {scores[task]:.4f}")
return scores
# ── Main ───────────────────────────────────────────────────────────────────────
def main():
os.makedirs(OUTPUT_DIR, exist_ok=True)
if not env_health(): print(f"❌ Env not reachable at {ENV_BASE_URL}"); return
print(f"βœ… Environment live at {ENV_BASE_URL}")
model, tokenizer = FastLanguageModel.from_pretrained(
MODEL_NAME, max_seq_length=MAX_SEQ_LEN, dtype=None, load_in_4bit=True)
model = FastLanguageModel.get_peft_model(
model, r=LORA_RANK,
target_modules=["q_proj","k_proj","v_proj","o_proj","gate_proj","up_proj","down_proj"],
lora_alpha=LORA_RANK, lora_dropout=0, bias="none",
use_gradient_checkpointing="unsloth", random_state=42)
FastLanguageModel.for_inference(model)
baseline = evaluate(model, tokenizer, "BASELINE (pre-training)")
with open(f"{OUTPUT_DIR}/baseline_scores.json","w") as f: json.dump(baseline, f, indent=2)
FastLanguageModel.for_training(model)
dataset = build_dataset(n=10)
print(f"\nDataset: {len(dataset)} prompts")
dataset_list = dataset.to_list()
def reward_wrapper(completions, prompts=None, **kwargs):
return reward_fn(completions, prompts=prompts, dataset_ref=dataset_list)
trainer = GRPOTrainer(
model=model,
args=GRPOConfig(
output_dir=OUTPUT_DIR,
num_train_epochs=3,
per_device_train_batch_size=2,
gradient_accumulation_steps=2, # ← lower than before β†’ more steps, matches Run 2 ~100
learning_rate=5e-6,
max_grad_norm=1.0, # ← explicit clip β†’ prevents NaN grad_norm
num_generations=4,
max_completion_length=384,
temperature=0.7,
logging_steps=5,
save_steps=25,
report_to="none",
# T4 FIX: disable mixed precision
fp16=False, # ← CHANGED: always False on T4
bf16=False, # ← CHANGED: T4 has no bfloat16
),
train_dataset=dataset,
reward_funcs=reward_wrapper,
processing_class=tokenizer,
)
print("\nπŸš€ Starting GRPO training...")
trainer.train()
save_path = "/content/aws-security-auditor-lora"
model.save_pretrained_merged(save_path, tokenizer, save_method="lora")
print(f"βœ… Model saved β†’ {save_path}")
# Push to HF Hub
if HF_TOKEN:
from huggingface_hub import HfApi
HfApi().upload_folder(
folder_path=save_path,
repo_id=HF_REPO,
token=HF_TOKEN,
commit_message="Restore: GRPO v3 β€” original 3-task grading, Run2-config (n=10, max_grad_norm=1.0)"
)
print(f"βœ… Pushed β†’ {HF_REPO}")
FastLanguageModel.for_inference(model)
trained = evaluate(model, tokenizer, "FINAL EVALUATION (post-training)")
print("\nπŸ“Š Before vs After:")
print(f"{'Task':<30} {'Baseline':>10} {'Trained':>10} {'Delta':>10}")
print("-"*62)
for task in CURRICULUM:
b, t = baseline.get(task, 0.0), trained.get(task, 0.0)
print(f"{task:<30} {b:>10.4f} {t:>10.4f} {'βœ…' if t>b else '⚠️'} {t-b:>+8.4f}")
with open(f"{OUTPUT_DIR}/training_comparison.json","w") as f:
json.dump({"baseline": baseline, "trained": trained}, f, indent=2)
print("\nβœ… Done.")
if __name__ == "__main__": main()