With baseline numbers in hand, run a short adaptation sprint. Default recommendation for this lab: LoRA SFT on your chat JSONL. Full fine-tuning is optional only if you already know PEFT underfits and you have the hardware. You can complete the learning goals with dry-run configs and toy loops if GPUs are unavailable.
Sprint means constrained ambition:
| Choose LoRA when | Consider fuller updates when |
|---|---|
| Weekend / single GPU | Proven PEFT ceiling with good data |
| Format/style/triage tasks | Huge domain shift + lots of data |
| Need modular rollback | Dedicated replica already planned |
For BackbenchLearner labs, pick LoRA unless your instructor says otherwise.
run_id = task_lora_r16_lr2e-4_e1.r=16, alpha=32, targets q_proj,v_proj (expand if needed), LR 2e-4, epochs 1.Still do the sprint paperwork:
The habit is the skill; GPUs only accelerate the same loop.
Dry-run config + a micro training loop that early-stops on val loss (CPU toy).
from dataclasses import dataclass, asdict
import json
@dataclass
class LoraSprintConfig:
run_id: str
base_model: str
r: int = 16
alpha: int = 32
lr: float = 2e-4
epochs: int = 2
targets: tuple[str, ...] = ("q_proj", "v_proj")
cfg = LoraSprintConfig(
run_id="triage_lora_r16_lab",
base_model="instruct-7b-base",
)
print(json.dumps(asdict(cfg), indent=2))
def toy_train(train_losses, val_losses, patience=1):
"""Emulate early stopping on val loss."""
best_i, best_v = None, float("inf")
bad = 0
history = []
for i, (tr, va) in enumerate(zip(train_losses, val_losses)):
history.append({"epoch": i, "train": tr, "val": va})
if va < best_v:
best_v, best_i, bad = va, i, 0
else:
bad += 1
if bad > patience:
break
return best_i, history
best, hist = toy_train([1.2, 0.9, 0.7, 0.5], [1.1, 0.95, 0.96, 1.05])
print("best_epoch", best, "history", hist)
HuggingFace-style launch sketch (illustrative):
# peft_config = LoraConfig(r=cfg.r, lora_alpha=cfg.alpha, target_modules=list(cfg.targets))
# model = get_peft_model(base, peft_config)
# trainer = Trainer(model=model, args=args, train_dataset=train_ds, eval_dataset=val_ds)
# trainer.train()
# model.save_pretrained(f"artifacts/{cfg.run_id}")
Monitor mentally (or in logs):
trainable% should be << 1% for LoRA
train loss falling
val schema_rate rising
anchor_pass not collapsing
Write the symptom → check mapping in your notes; it will save the second experiment.
If you only have a laptop CPU, still produce: validated JSONL, config file, toy loop proving you understand gradients on adapters, and a filled scorecard with "simulated" or API-evaluated numbers if you call a hosted base model for baselines. The sprint grades process rigor.
Add a second run that changes exactly one knob (rank 16 → 32). Compare val curves. That single ablation teaches more than five unrelated blog configs.
Even in a short sprint, log: timestamp, run_id, step, train_loss, val_schema_rate, val_intent_acc, anchor_pass, lr, epoch. A CSV with those columns is enough to reconstruct why you pinned a checkpoint. If you only save the final adapter, you cannot explain the curve later.
If two people tune prompts while one trains, stop. Parallel edits to system_prompt.txt invalidate the experiment. Appoint one owner for the frozen prompt during the sprint window.
Run a named LoRA SFT sprint with a fixed recipe, validation-based early stopping, and checkpoints you can score—holdout stays sealed until the writeup.