Skip to content

Commit 61daa10

Browse files
committed
test(v4-6): all 6 schedule kinds through UI→train with shape assertions
Closes V4-6 / cppmega-mlx-08b. V3-5 only proved linear_warmup; v4-6 covers the full set: constant, linear_warmup, cosine, wsd, inv_sqrt, polynomial. Each scenario sets required ScheduleEditor fields per kind (warmup / total / decay / power), Apply, Train N=8, asserts: - extras.schedule_kind matches selection - extras.model_summary.schedule_kind matches - lr_trajectory.length === 8 - Shape per kind: constant: all equal linear_warmup w=4: lr[0]=0, lr[4]>lr[0] cosine total=8: monotone decay after warmup wsd: warmup → stable → decay inv_sqrt: ramp then 1/√t decay polynomial: (1-t/T)^power decay 6/6 green in 7.6s.
1 parent 5a12091 commit 61daa10

1 file changed

Lines changed: 126 additions & 0 deletions

File tree

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// V4-6: All 6 schedule kinds reach stage_train and shape lr_trajectory
2+
// correctly. V3-5 proved only linear_warmup; v4-6 covers the remaining
3+
// 5 + verifies analytical trajectory shape per kind.
4+
5+
import { test, expect } from "@playwright/test";
6+
import { gotoApp, selectPreset, closeModal } from "../fixtures";
7+
import { readTrainExtras } from "../utils/train_extras";
8+
9+
type Setup = {
10+
warmup?: string; // for non-constant schedules
11+
total?: string; // cosine / wsd / polynomial
12+
decay?: string; // wsd only
13+
power?: string; // polynomial only
14+
};
15+
16+
interface Scenario {
17+
kind: string;
18+
setup: Setup;
19+
/** Asserts lr_trajectory shape for N=8 steps with base_lr governed
20+
* by spec default group 0 lr (3e-4 for AdamW). */
21+
assertTrajectory: (lr: number[]) => void;
22+
}
23+
24+
const SCENARIOS: Scenario[] = [
25+
{
26+
kind: "constant",
27+
setup: {},
28+
assertTrajectory: (lr) => {
29+
// All values equal (within rounding to 6 dp).
30+
const first = lr[0];
31+
for (const v of lr) expect(v).toBeCloseTo(first, 6);
32+
},
33+
},
34+
{
35+
kind: "linear_warmup",
36+
setup: { warmup: "4" },
37+
assertTrajectory: (lr) => {
38+
// First step is ramp from 0; step 4+ is at base_lr.
39+
expect(lr[0]).toBeCloseTo(0, 6);
40+
expect(lr[4]).toBeGreaterThan(lr[0] + 1e-7);
41+
expect(lr[4]).toBeGreaterThanOrEqual(lr[3]);
42+
},
43+
},
44+
{
45+
kind: "cosine",
46+
setup: { warmup: "2", total: "8" },
47+
assertTrajectory: (lr) => {
48+
// After warmup, lr monotonically decays toward min_lr_ratio*base.
49+
expect(lr[0]).toBeCloseTo(0, 6);
50+
expect(lr[7]).toBeLessThan(lr[2]);
51+
},
52+
},
53+
{
54+
kind: "wsd",
55+
setup: { warmup: "2", total: "8", decay: "4" },
56+
assertTrajectory: (lr) => {
57+
// Warmup → stable → decay. Last < peak.
58+
expect(lr[0]).toBeCloseTo(0, 6);
59+
expect(lr[7]).toBeLessThan(Math.max(...lr));
60+
},
61+
},
62+
{
63+
kind: "inv_sqrt",
64+
setup: { warmup: "2" },
65+
assertTrajectory: (lr) => {
66+
// Warmup ramps then 1/sqrt decay.
67+
expect(lr[0]).toBeCloseTo(0, 6);
68+
expect(lr[7]).toBeLessThan(lr[2] + 1e-9);
69+
},
70+
},
71+
{
72+
kind: "polynomial",
73+
setup: { warmup: "2", total: "8", power: "2" },
74+
assertTrajectory: (lr) => {
75+
// (1 - t/T)^power monotonic decay after warmup.
76+
expect(lr[0]).toBeCloseTo(0, 6);
77+
expect(lr[7]).toBeLessThan(lr[2]);
78+
},
79+
},
80+
];
81+
82+
for (const { kind, setup, assertTrajectory } of SCENARIOS) {
83+
test(`V4-6: schedule '${kind}' propagates kind + lr_trajectory shape`,
84+
async ({ page }) => {
85+
test.setTimeout(60_000);
86+
await gotoApp(page);
87+
await selectPreset(page, "llama3_8b");
88+
89+
await page.getByTestId("sidebar-tab-optim").click();
90+
await page.getByTestId("optim-group-0-schedule-toggle").click();
91+
await page.getByTestId("schedule-kind-0").selectOption(kind);
92+
if (setup.warmup) {
93+
await page.getByTestId("schedule-warmup-0").fill(setup.warmup);
94+
}
95+
if (setup.total) {
96+
await page.getByTestId("schedule-total-0").fill(setup.total);
97+
}
98+
if (setup.decay) {
99+
await page.getByTestId("schedule-decay-0").fill(setup.decay);
100+
}
101+
if (setup.power) {
102+
await page.getByTestId("schedule-power-0").fill(setup.power);
103+
}
104+
await page.getByTestId("optim-apply").click();
105+
106+
// Run Train with N=8 so trajectory shape is observable.
107+
await page.getByTestId("run-pipeline-toggle").click();
108+
await page.getByTestId("train-num-steps").fill("8");
109+
await page.getByTestId("run-pipeline-train").click();
110+
const modal = page.getByTestId("run-result-modal");
111+
await modal.waitFor({ timeout: 60_000 });
112+
113+
const extras = await readTrainExtras(page);
114+
115+
// Propagation
116+
expect(extras.schedule_kind).toBe(kind);
117+
expect(extras.model_summary.schedule_kind).toBe(kind);
118+
expect(extras.num_steps).toBe(8);
119+
expect(extras.lr_trajectory.length).toBe(8);
120+
121+
// Shape
122+
assertTrajectory(extras.lr_trajectory);
123+
124+
await closeModal(page);
125+
});
126+
}

0 commit comments

Comments
 (0)