# GPU Goblin canonical demo workload. # # Qwen2.5-7B-Instruct + LoRA fine-tune on the alpaca-cleaned dataset, staged # with *deliberately* sub-optimal defaults so the goblin has something to fix # in the demo. This script does NOT need to actually execute on a host — it # exists so `parse_config` can extract a realistic WorkloadConfig from it. # # Expected findings when audited: # - precision.bf16_over_fp16_on_mi300x (fp16=True) # - attention.flash_rocm_over_eager (attn_implementation="eager") # - data.dataloader_workers_zero (dataloader_num_workers=0) # - memory.batch_too_small_for_192gb (per_device_train_batch_size=4) import os import sys import time # Bootstrap the repo root onto sys.path so `from workloads._runtime import ...` # works regardless of where the script is invoked from. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import torch from datasets import load_dataset from peft import LoraConfig, get_peft_model from torch.utils.data import DataLoader from transformers import ( AutoModelForCausalLM, AutoTokenizer, Trainer, TrainingArguments, ) from workloads._runtime import emit_torch_profile, parse_runtime_args # Parse the goblin_runner.sh injected flags (--max_steps, --torch_profile_out). _runtime = parse_runtime_args() # A redactable secret so parse_config has something to scrub during the demo. os.environ["HF_TOKEN"] = "hf_aaaaaaaaaaaaaaaaaaaaaaaaaaaaa" HF_TOKEN = os.environ["HF_TOKEN"] # ROCm-flavored env knobs the agent should pick up into env_vars. os.environ["HSA_FORCE_FINE_GRAIN_PCIE"] = "1" os.environ["MIOPEN_FIND_MODE"] = "3" MODEL_ID = "Qwen/Qwen2.5-7B-Instruct" tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, token=HF_TOKEN) model = AutoModelForCausalLM.from_pretrained( MODEL_ID, torch_dtype=torch.float16, attn_implementation="eager", # naive attention -- goblin should swap to flash_rocm token=HF_TOKEN, ) # LoRA — rank 16, attached to attention projections. lora_config = LoraConfig( r=16, lora_alpha=32, target_modules=["q_proj", "v_proj"], lora_dropout=0.05, bias="none", task_type="CAUSAL_LM", ) model = get_peft_model(model, lora_config) dataset = load_dataset("yahma/alpaca-cleaned", split="train") # Hand-rolled DataLoader so parse_config sees the dataloader kwargs explicitly. # NOTE: PyTorch raises ValueError if you set prefetch_factor while num_workers=0 # ("could only be specified in multiprocessing"). The audit is supposed to # spot this misconfiguration, not crash on it — so the canonical demo keeps # num_workers=0 (the deliberate badness) and lets prefetch_factor default, # which parse_config will see as `dataloader_prefetch_factor=None`. The KB # rule data.prefetch_factor_default still fires once num_workers is bumped. train_loader = DataLoader( dataset, batch_size=4, num_workers=0, # leaves the GPU starved during training -- data_wait waste pin_memory=False, persistent_workers=False, ) # NOTE: keep TrainingArguments(...) called with LITERAL kwargs — parse_config # walks the AST and only extracts kwargs whose values are literals (or simple # identifiers it has seen). A `**dict_var` splat or a runtime expression hides # the value from the parser, which then falls back to HF defaults # (batch_size=1, lr=5e-5, etc.) and the agent reasons over the wrong config. # `--max_steps` is the one runtime override, but it isn't a WorkloadConfig # field so the parser doesn't need to see it; passing it as an expression is # fine. _RUNTIME_MAX_STEPS = _runtime.max_steps if _runtime.max_steps > 0 else -1 training_args = TrainingArguments( output_dir="./out", per_device_train_batch_size=4, # leaves HBM on the floor at 192 GB gradient_accumulation_steps=8, num_train_epochs=3, learning_rate=2e-4, warmup_steps=100, fp16=True, # bf16 is the right call on CDNA3 optim="adamw_torch", logging_steps=10, save_steps=500, dataloader_num_workers=0, dataloader_pin_memory=False, gradient_checkpointing=False, torch_compile=False, report_to="none", push_to_hub=False, # Alpaca columns are [instruction, input, output] — keep them so our # toy collator below can still see them. Without this, HF Trainer drops # them and the dataset becomes empty before forward(). This is purely a # fix-the-script-so-rocprofv3-can-actually-trace-it concern; it has no # bearing on the audit's findings. remove_unused_columns=False, # Runtime override only (parser ignores non-literals; HF Trainer treats # max_steps=-1 as "use num_train_epochs"): max_steps=_RUNTIME_MAX_STEPS, ) # Tiny collator turning the alpaca rows into input_ids / labels so the # Trainer can call forward(). It's intentionally trivial — the goal is to # be runnable enough for rocprofv3 to capture a few real training steps, # not to actually train anything useful. def _toy_collate(rows): texts = [ (r.get("instruction") or "") + ("\n" + r["input"] if r.get("input") else "") + "\n" + (r.get("output") or "") for r in rows ] enc = tokenizer( texts, padding=True, truncation=True, max_length=512, return_tensors="pt", ) enc["labels"] = enc["input_ids"].clone() return enc trainer = Trainer( model=model, args=training_args, train_dataset=dataset, tokenizer=tokenizer, data_collator=_toy_collate, ) if __name__ == "__main__": # Total parameter count drives MFU. For a peft-wrapped model this # includes both the frozen base (which still does forward) and the # tiny LoRA adapters. The forward+backward FLOPs estimate uses the # full count (the standard 6N rule lives inside emit_torch_profile). _model_params = sum(p.numel() for p in model.parameters()) _t0 = time.time() trainer.train() _elapsed = time.time() - _t0 # trainer.state.global_step is the actual number of optimization steps # (honors max_steps); fall back to the runtime arg if the trainer # bailed before bumping it. _n_steps = int(getattr(trainer.state, "global_step", 0) or _runtime.max_steps) emit_torch_profile( _runtime.torch_profile_out, elapsed=_elapsed, n_steps=_n_steps, per_device_batch=training_args.per_device_train_batch_size, grad_accum=training_args.gradient_accumulation_steps, seq_len_cap=512, model_params=_model_params, )