opsguard / scripts /bold_pipeline.py
sai1906's picture
cuda: sleep 30s instead of poll-fail; let model load handle lazy init
b3b0fcf verified
Raw
History Blame Contribute Delete
19.3 kB
"""Bold full-stack training pipeline: SFT -> DPO -> GRPO -> Adv DPO -> Eval.
ONE script. Two modes via --smoke flag:
--smoke : 5 steps per stage, ~10 min total. Validates code path. Free Colab.
(default) : full step counts, ~3-4h on H200. Production.
Usage in Colab T4 (smoke):
!python scripts/bold_pipeline.py --smoke
Usage on HF Jobs H200 (production):
hf jobs uv run --flavor h200 \\
https://huggingface.co/spaces/sai1906/opsguard/resolve/main/scripts/bold_pipeline.py
"""
from __future__ import annotations
import argparse
import gc
import inspect
import json
import os
import random
import sys
from pathlib import Path
def _wait_for_cuda():
import time as _t
print("[cuda] sleeping 30s for driver init...", flush=True)
_t.sleep(30)
try:
import torch
n = torch.cuda.device_count() if torch.cuda.is_available() else 0
if n > 0:
print(f"[cuda] ready: {n} device(s), {torch.cuda.get_device_name(0)}", flush=True)
else:
print("[cuda] device_count=0 after sleep — letting model load trigger lazy init", flush=True)
except Exception as e:
print(f"[cuda] probe error (non-fatal, model load will retry): {e}", flush=True)
def _setup_workdir():
import subprocess
work = Path("/tmp/opsguard")
if not (work / "data" / "sft_traces.jsonl").exists():
if work.exists():
subprocess.run(["rm", "-rf", str(work)], check=True)
token = os.environ.get("HF_TOKEN", "")
url = (f"https://user:{token}@huggingface.co/spaces/sai1906/opsguard"
if token else "https://huggingface.co/spaces/sai1906/opsguard")
subprocess.run(["git", "clone", "--depth", "1", url, str(work)], check=True)
sys.path.insert(0, str(work))
os.chdir(work)
needed = work / "data" / "sft_traces.jsonl"
if not needed.exists():
raise FileNotFoundError(
f"{needed} missing after clone. cwd={os.getcwd()} "
f"contents={list(work.iterdir()) if work.exists() else 'workdir-missing'}"
)
print(f"[workdir] cwd={os.getcwd()} data files OK", flush=True)
return work
_wait_for_cuda()
WORK = _setup_workdir()
def _safe_kwargs(cls, kw):
sig = inspect.signature(cls).parameters
return {k: v for k, v in kw.items() if k in sig}
def _free():
import torch
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()
def _push_lora(folder, repo_id):
from huggingface_hub import HfApi
api = HfApi(token=os.environ["HF_TOKEN"])
try:
api.create_repo(repo_id, repo_type="model", exist_ok=True)
except Exception as e:
print(f" create_repo({repo_id}): {e}", flush=True)
api.upload_folder(folder_path=folder, repo_id=repo_id, repo_type="model")
print(f" pushed -> https://huggingface.co/{repo_id}", flush=True)
def stage_sft(args, model_name, hub_repo):
print("\n" + "=" * 70 + "\n=== STAGE 1: SFT WARMSTART ===\n" + "=" * 70, flush=True)
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from datasets import Dataset
from trl import SFTConfig, SFTTrainer
print(f"loading {model_name} (4bit={args.use_4bit})...", flush=True)
model_kwargs = {"torch_dtype": torch.bfloat16, "device_map": "auto",
"attn_implementation": "sdpa"}
if args.use_4bit:
bnb = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True, bnb_4bit_quant_type="nf4")
model_kwargs["quantization_config"] = bnb
tok = AutoTokenizer.from_pretrained(model_name)
if tok.pad_token_id is None:
tok.pad_token_id = tok.eos_token_id
model = AutoModelForCausalLM.from_pretrained(model_name, **model_kwargs)
model.config.use_cache = False
if args.use_4bit:
model = prepare_model_for_kbit_training(model, use_gradient_checkpointing=True)
else:
model.gradient_checkpointing_enable(gradient_checkpointing_kwargs={"use_reentrant": False})
model.enable_input_require_grads()
lc = LoraConfig(r=args.lora_r, lora_alpha=args.lora_alpha, lora_dropout=0.05, bias="none",
target_modules=["q_proj","k_proj","v_proj","o_proj",
"gate_proj","up_proj","down_proj"],
task_type="CAUSAL_LM")
model = get_peft_model(model, lc)
for n, p in model.named_parameters():
if p.requires_grad and ("lora_" in n or "lora_A" in n or "lora_B" in n):
p.data = p.data.to(torch.float32)
model.print_trainable_parameters()
rows = [json.loads(l) for l in open("data/sft_traces.jsonl")]
print(f"loaded {len(rows)} SFT traces", flush=True)
texts = [r["prompt"] + "\n\nACTION:\n" + r["completion"] + tok.eos_token for r in rows]
if args.smoke:
texts = texts[:64]
ds = Dataset.from_list([{"text": t} for t in texts])
n_epochs = 1 if args.smoke else args.sft_epochs
raw = dict(
output_dir="/tmp/opsguard-sft",
per_device_train_batch_size=args.batch_size,
gradient_accumulation_steps=args.grad_accum,
num_train_epochs=n_epochs,
learning_rate=2e-5,
max_grad_norm=1.0,
weight_decay=0.01,
warmup_ratio=0.1,
adam_epsilon=1e-7,
optim="adamw_torch",
gradient_checkpointing=True,
gradient_checkpointing_kwargs={"use_reentrant": False},
logging_steps=2,
save_strategy="epoch",
save_total_limit=1,
bf16=True,
max_seq_length=2048,
dataset_text_field="text",
report_to="none",
push_to_hub=False,
max_steps=5 if args.smoke else -1,
)
cfg = SFTConfig(**_safe_kwargs(SFTConfig, raw))
trainer = SFTTrainer(model=model, train_dataset=ds, args=cfg, processing_class=tok)
trainer.train()
out_path = "/tmp/opsguard-sft-lora"
model.save_pretrained(out_path)
tok.save_pretrained(out_path)
if hub_repo and not args.smoke:
_push_lora(out_path, hub_repo + "-sft")
del trainer, model
_free()
return out_path
def stage_dpo(args, model_name, sft_lora_path, hub_repo):
print("\n" + "=" * 70 + "\n=== STAGE 2: DPO (R2Vul-style preference pairs) ===\n" + "=" * 70, flush=True)
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import PeftModel
from datasets import Dataset
from trl import DPOConfig, DPOTrainer
pref_path = "data/preference_pairs.jsonl"
if not Path(pref_path).exists():
print(f"WARN: {pref_path} missing — skipping DPO", flush=True)
return sft_lora_path
rows = [json.loads(l) for l in open(pref_path)]
print(f"loaded {len(rows)} preference pairs", flush=True)
if args.smoke:
rows = rows[:32]
ds = Dataset.from_list([
{"prompt": r["prompt"], "chosen": r["chosen"], "rejected": r["rejected"]}
for r in rows
])
model_kwargs = {"torch_dtype": torch.bfloat16, "device_map": "auto",
"attn_implementation": "sdpa"}
if args.use_4bit:
bnb = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True, bnb_4bit_quant_type="nf4")
model_kwargs["quantization_config"] = bnb
tok = AutoTokenizer.from_pretrained(sft_lora_path)
base = AutoModelForCausalLM.from_pretrained(model_name, **model_kwargs)
base.config.use_cache = False
if not args.use_4bit:
base.gradient_checkpointing_enable(gradient_checkpointing_kwargs={"use_reentrant": False})
base.enable_input_require_grads()
model = PeftModel.from_pretrained(base, sft_lora_path, is_trainable=True)
for n, p in model.named_parameters():
if p.requires_grad and "lora_" in n:
p.data = p.data.to(torch.float32)
n_epochs = 1 if args.smoke else args.dpo_epochs
raw = dict(
output_dir="/tmp/opsguard-dpo",
per_device_train_batch_size=1,
gradient_accumulation_steps=8,
num_train_epochs=n_epochs,
learning_rate=5e-6,
max_grad_norm=1.0,
weight_decay=0.01,
adam_epsilon=1e-7,
optim="adamw_torch",
gradient_checkpointing=True,
gradient_checkpointing_kwargs={"use_reentrant": False},
beta=0.1,
max_length=2048,
max_prompt_length=1500,
logging_steps=2,
save_strategy="epoch",
save_total_limit=1,
bf16=True,
report_to="none",
push_to_hub=False,
remove_unused_columns=False,
max_steps=5 if args.smoke else -1,
)
cfg = DPOConfig(**_safe_kwargs(DPOConfig, raw))
try:
trainer = DPOTrainer(model=model, args=cfg, train_dataset=ds, processing_class=tok)
except TypeError:
trainer = DPOTrainer(model=model, args=cfg, train_dataset=ds, tokenizer=tok)
trainer.train()
out_path = "/tmp/opsguard-dpo-lora"
model.save_pretrained(out_path)
tok.save_pretrained(out_path)
if hub_repo and not args.smoke:
_push_lora(out_path, hub_repo + "-dpo")
del trainer, model, base
_free()
return out_path
def stage_grpo(args, model_name, dpo_lora_path, hub_repo):
print("\n" + "=" * 70 + "\n=== STAGE 3: GRPO via custom reward fn (no TRL OpenEnv tools) ===\n" + "=" * 70, flush=True)
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import PeftModel
from datasets import Dataset
from trl import GRPOConfig, GRPOTrainer
from server.opsguard_environment import OpsguardEnvironment
from models import OpsguardAction, ActionType
from scripts.system_prompt import SYSTEM_PROMPT, format_observation, parse_action
model_kwargs = {"torch_dtype": torch.bfloat16, "device_map": "auto",
"attn_implementation": "sdpa"}
if args.use_4bit:
bnb = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True, bnb_4bit_quant_type="nf4")
model_kwargs["quantization_config"] = bnb
tok = AutoTokenizer.from_pretrained(dpo_lora_path)
if tok.pad_token_id is None:
tok.pad_token_id = tok.eos_token_id
base = AutoModelForCausalLM.from_pretrained(model_name, **model_kwargs)
base.config.use_cache = False
if not args.use_4bit:
base.gradient_checkpointing_enable(gradient_checkpointing_kwargs={"use_reentrant": False})
base.enable_input_require_grads()
model = PeftModel.from_pretrained(base, dpo_lora_path, is_trainable=True)
for n, p in model.named_parameters():
if p.requires_grad and "lora_" in n:
p.data = p.data.to(torch.float32)
print("building prompt dataset (sampled env observations)...", flush=True)
n_prompts = 24 if args.smoke else args.grpo_n_prompts
scenarios = ["E2_social_eng_buildup", "E3_compromised_maintainer", "E4_multi_vector"]
prompt_rows = []
env = OpsguardEnvironment()
for i in range(n_prompts):
sid = scenarios[i % len(scenarios)]
obs = env.reset(scenario_id=sid, seed=i)
for _ in range(random.randint(0, 5)):
obs = env.step(OpsguardAction(action_type=ActionType.WAIT))
if obs.done:
obs = env.reset(scenario_id=sid, seed=i)
break
prompt_text = SYSTEM_PROMPT + "\n\nOBSERVATION:\n" + format_observation(obs) + "\n\nACTION:\n"
prompt_rows.append({"prompt": prompt_text, "scenario": sid, "seed": i})
ds = Dataset.from_list(prompt_rows)
def reward_fn(completions, prompts=None, scenario=None, seed=None, **kwargs):
rewards = []
scenarios_b = scenario if scenario is not None else ["E2_social_eng_buildup"] * len(completions)
seeds_b = seed if seed is not None else [0] * len(completions)
for comp, sid, sd in zip(completions, scenarios_b, seeds_b):
try:
action = parse_action(comp)
env_local = OpsguardEnvironment()
env_local.reset(scenario_id=sid, seed=int(sd))
obs = env_local.step(action)
rewards.append(float(obs.reward) if obs.reward is not None else 0.0)
except Exception:
rewards.append(-1.0)
return rewards
n_steps = 5 if args.smoke else args.grpo_steps
raw = dict(
output_dir="/tmp/opsguard-grpo",
per_device_train_batch_size=1,
gradient_accumulation_steps=4,
num_generations=args.grpo_num_generations,
max_steps=n_steps,
max_completion_length=256,
max_prompt_length=1500,
beta=0.001,
learning_rate=5e-6,
max_grad_norm=1.0,
weight_decay=0.01,
adam_epsilon=1e-7,
optim="adamw_torch",
gradient_checkpointing=True,
gradient_checkpointing_kwargs={"use_reentrant": False},
logging_steps=1,
save_strategy="no",
bf16=True,
report_to="none",
push_to_hub=False,
use_vllm=False,
)
cfg = GRPOConfig(**_safe_kwargs(GRPOConfig, raw))
try:
trainer = GRPOTrainer(model=model, args=cfg, reward_funcs=reward_fn,
train_dataset=ds, processing_class=tok)
except TypeError:
trainer = GRPOTrainer(model=model, args=cfg, reward_funcs=reward_fn,
train_dataset=ds, tokenizer=tok)
trainer.train()
out_path = "/tmp/opsguard-grpo-lora"
model.save_pretrained(out_path)
tok.save_pretrained(out_path)
if hub_repo and not args.smoke:
_push_lora(out_path, hub_repo + "-grpo")
del trainer, model, base
_free()
return out_path
def stage_eval(args, model_name, final_lora_path):
print("\n" + "=" * 70 + "\n=== STAGE 4: EVAL — trained vs base on Datadog held-out ===\n" + "=" * 70, flush=True)
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import PeftModel
from models import OpsguardAction, ActionType
from scripts.system_prompt import SYSTEM_PROMPT, format_observation, parse_action
cve_rows = [json.loads(l) for l in open("data/datadog_extracted.jsonl")]
if args.smoke:
cve_rows = cve_rows[:20]
print(f"evaluating on {len(cve_rows)} real Datadog samples...", flush=True)
model_kwargs = {"torch_dtype": torch.bfloat16, "device_map": "auto"}
if args.use_4bit:
bnb = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True, bnb_4bit_quant_type="nf4")
model_kwargs["quantization_config"] = bnb
tok = AutoTokenizer.from_pretrained(final_lora_path)
base = AutoModelForCausalLM.from_pretrained(model_name, **model_kwargs)
model = PeftModel.from_pretrained(base, final_lora_path)
model.eval()
@torch.inference_mode()
def predict(text_payload):
prompt = SYSTEM_PROMPT + "\n\nOBSERVATION:\n" + text_payload + "\n\nACTION:\n"
inp = tok(prompt, return_tensors="pt", truncation=True, max_length=1800).to(model.device)
out = model.generate(**inp, max_new_tokens=200, do_sample=False,
pad_token_id=tok.eos_token_id)
return tok.decode(out[0][inp["input_ids"].shape[1]:], skip_special_tokens=True)
REJECT = {"reject_pr", "close_spam", "flag_security"}
n_caught = 0
for r in cve_rows:
synth_obs = json.dumps({
"current_issue": {
"title": r.get("package", "") + " " + r.get("filename", ""),
"body": r.get("diff_preview", "")[:1000],
"is_pr": True,
"author_login": "unknown",
"pr_diff_preview": r.get("diff_preview", "")[:800],
}
})
try:
text = predict(synth_obs)
action = parse_action(text)
at = action.action_type.value if hasattr(action.action_type, "value") else str(action.action_type)
if at in REJECT:
n_caught += 1
except Exception:
pass
catch_rate = n_caught / max(1, len(cve_rows))
print(f"\nFINAL: trained LoRA caught {n_caught}/{len(cve_rows)} = {catch_rate:.3f} catch rate on Datadog held-out", flush=True)
Path("/tmp/eval_post_train").mkdir(exist_ok=True)
Path("/tmp/eval_post_train/cve_results.json").write_text(json.dumps({
"n_samples": len(cve_rows),
"n_caught": n_caught,
"catch_rate": round(catch_rate, 3),
}))
return catch_rate
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--smoke", action="store_true",
help="5 steps per stage, ~10 min on Colab T4. Validates code path.")
ap.add_argument("--model", type=str, default="Qwen/Qwen2.5-7B-Instruct")
ap.add_argument("--use-4bit", action="store_true",
help="Use bnb 4-bit (default false = bf16 full). Toggle for VRAM-constrained env.")
ap.add_argument("--lora-r", type=int, default=32,
help="LoRA rank (default 32 for stability). 128 for bold.")
ap.add_argument("--lora-alpha", type=int, default=64,
help="LoRA alpha (default 64 = 2*r, mild scaling).")
ap.add_argument("--sft-epochs", type=int, default=5)
ap.add_argument("--dpo-epochs", type=int, default=3)
ap.add_argument("--grpo-steps", type=int, default=200)
ap.add_argument("--grpo-num-generations", type=int, default=4)
ap.add_argument("--grpo-n-prompts", type=int, default=256)
ap.add_argument("--batch-size", type=int, default=2)
ap.add_argument("--grad-accum", type=int, default=4)
ap.add_argument("--hub-repo", type=str, default="sai1906/opsguard",
help="Hub repo prefix; suffixes -sft, -dpo, -grpo appended.")
ap.add_argument("--skip", type=str, default="",
help="Comma-separated stages to skip: sft,dpo,grpo,eval")
args = ap.parse_args()
if args.smoke and not args.use_4bit:
print("[smoke] forcing --use-4bit on Colab/free GPUs", flush=True)
args.use_4bit = True
skipped = set(s.strip() for s in args.skip.split(",") if s.strip())
print(f"=== BOLD PIPELINE smoke={args.smoke} 4bit={args.use_4bit} "
f"r={args.lora_r} alpha={args.lora_alpha} ===", flush=True)
print(f" SFT epochs={args.sft_epochs} DPO epochs={args.dpo_epochs} GRPO steps={args.grpo_steps}", flush=True)
sft_lora = stage_sft(args, args.model, args.hub_repo) if "sft" not in skipped else None
dpo_lora = stage_dpo(args, args.model, sft_lora or args.model, args.hub_repo) if "dpo" not in skipped else sft_lora
grpo_lora = stage_grpo(args, args.model, dpo_lora or sft_lora or args.model, args.hub_repo) if "grpo" not in skipped else dpo_lora
final = grpo_lora or dpo_lora or sft_lora
if final and "eval" not in skipped:
stage_eval(args, args.model, final)
print("\n=== DONE ===", flush=True)
if __name__ == "__main__":
main()