feat: add cosine LR decay schedule to trainer and scheduler

- Add lr_schedule param (constant|cosine) to SelvaLoraTrainer
- Cosine decays LR from initial value to ~0 after warmup, preventing
  the oscillation observed at steps 6000-8000 with lr=2e-4 flat
- Wire lr_schedule through scheduler _PARAM_DEFAULTS and _train_inner call
- Add g5_r128_lr_2e4_cosine and g5_r128_lr_3e4_cosine to r128_sweet_spot sweep

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-08 13:25:01 +02:00
parent 58e1985af2
commit 1be07a80d2
3 changed files with 41 additions and 5 deletions
+4
View File
@@ -78,6 +78,7 @@ _PARAM_DEFAULTS = {
"curriculum_switch": 0.6,
"lora_dropout": 0.0,
"lora_plus_ratio": 1.0,
"lr_schedule": "constant",
}
# Palette for comparison chart: one color per experiment (cycles if > 8)
@@ -386,6 +387,7 @@ class SelvaLoraScheduler:
curr_switch = float(cfg.get("curriculum_switch", 0.6))
dropout = float(cfg.get("lora_dropout", 0.0))
plus_ratio = float(cfg.get("lora_plus_ratio", 1.0))
lr_schedule = str(cfg.get("lr_schedule", "constant"))
alpha_val = alpha if alpha > 0.0 else float(rank)
target_suffixes = tuple(target.strip().split())
@@ -407,6 +409,7 @@ class SelvaLoraScheduler:
"timestep_mode": ts_mode, "logit_normal_sigma": ln_sigma,
"curriculum_switch": curr_switch,
"lora_dropout": dropout, "lora_plus_ratio": plus_ratio,
"lr_schedule": lr_schedule,
},
"results": {"status": "running"},
"adapter_path": None,
@@ -425,6 +428,7 @@ class SelvaLoraScheduler:
alpha_val, target_suffixes, batch_size, warmup,
grad_accum, save_every, resume_path, seed,
ts_mode, ln_sigma, curr_switch, dropout, plus_ratio,
lr_schedule,
)
duration = time.monotonic() - t_start
+22 -5
View File
@@ -1,5 +1,6 @@
import copy
import json
import math
import random
import traceback
from pathlib import Path
@@ -528,6 +529,13 @@ class SelvaLoraTrainer:
"tooltip": "LoRA+ LR ratio: lr_B = lr × ratio. "
"1.0 = standard LoRA. 16.0 = LoRA+ (arXiv:2402.12354).",
}),
"lr_schedule": (["constant", "cosine"], {
"default": "constant",
"tooltip": "LR schedule after warmup. "
"constant: flat LR for all steps. "
"cosine: decay from lr to ~0 following a cosine curve — "
"prevents oscillation when LR is slightly too high.",
}),
},
}
@@ -551,7 +559,7 @@ class SelvaLoraTrainer:
alpha=0.0, target="attn.qkv", batch_size=4, warmup_steps=100,
grad_accum=1, save_every=500, resume_path="", seed=42,
timestep_mode="uniform", logit_normal_sigma=1.0, curriculum_switch=0.6,
lora_dropout=0.0, lora_plus_ratio=1.0):
lora_dropout=0.0, lora_plus_ratio=1.0, lr_schedule="constant"):
torch.manual_seed(seed)
random.seed(seed)
@@ -601,7 +609,7 @@ class SelvaLoraTrainer:
alpha_val, target_suffixes, batch_size, warmup_steps,
grad_accum, save_every, resume_path, seed,
timestep_mode, logit_normal_sigma, curriculum_switch,
lora_dropout, lora_plus_ratio,
lora_dropout, lora_plus_ratio, lr_schedule,
)
return (r["patched_model"], r["adapter_path"], r["loss_curve"])
@@ -612,7 +620,7 @@ class SelvaLoraTrainer:
alpha_val, target_suffixes, batch_size, warmup_steps,
grad_accum, save_every, resume_path, seed,
timestep_mode="uniform", logit_normal_sigma=1.0, curriculum_switch=0.6,
lora_dropout=0.0, lora_plus_ratio=1.0,
lora_dropout=0.0, lora_plus_ratio=1.0, lr_schedule="constant",
):
# --- Prepare generator copy with LoRA ---
generator = copy.deepcopy(model["generator"]).to(device, dtype)
@@ -648,8 +656,16 @@ class SelvaLoraTrainer:
if lora_plus_ratio != 1.0:
print(f"[LoRA Trainer] LoRA+: lr_A={lr:.2e} lr_B={lr * lora_plus_ratio:.2e}", flush=True)
def lr_lambda(s):
return s / max(1, warmup_steps) if s < warmup_steps else 1.0
if lr_schedule == "cosine":
def lr_lambda(s):
if s < warmup_steps:
return s / max(1, warmup_steps)
progress = (s - warmup_steps) / max(1, steps - warmup_steps)
return max(1e-6 / lr, 0.5 * (1.0 + math.cos(math.pi * progress)))
print(f"[LoRA Trainer] LR schedule: cosine decay {lr:.2e} → 0", flush=True)
else:
def lr_lambda(s):
return s / max(1, warmup_steps) if s < warmup_steps else 1.0
scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda)
fm = FlowMatching(min_sigma=0, inference_mode="euler", num_steps=25)
@@ -701,6 +717,7 @@ class SelvaLoraTrainer:
"curriculum_switch": curriculum_switch,
"lora_dropout": lora_dropout,
"lora_plus_ratio": lora_plus_ratio,
"lr_schedule": lr_schedule,
}
# For curriculum mode: compute the step at which we switch from logit_normal to uniform