Skip to content

Commit 3c93823

Browse files
committed
fix: agent studio gradient descent + mlx lora stability @codex
1 parent d66e59e commit 3c93823

8 files changed

Lines changed: 89 additions & 31 deletions

File tree

server/retrieval/mlx_qwen3.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,20 @@ def __init__(self, base: Any, *, r: int, scale: float, dropout_p: float) -> None
129129
self.scale = float(scale)
130130
self.dropout = nn.Dropout(float(dropout_p)) if float(dropout_p) > 0.0 else None
131131

132+
# MLX `nn.Linear` exposes `weight` as (out, in). However MLX `nn.QuantizedLinear`
133+
# stores a packed `weight` matrix (out, packed_in) where packed_in = in / (32 / bits).
134+
# We need the *logical* dims for LoRA shapes.
132135
try:
133-
out_dim, in_dim = int(base.weight.shape[0]), int(base.weight.shape[1])
136+
qlinear = getattr(nn, "QuantizedLinear", None)
137+
if qlinear is not None and isinstance(base, qlinear):
138+
bits = int(getattr(base, "bits", 4) or 4)
139+
pack = max(1, int(32 // max(1, bits)))
140+
out_dim = int(base.weight.shape[0])
141+
in_dim = int(base.weight.shape[1]) * pack
142+
else:
143+
out_dim, in_dim = int(base.weight.shape[0]), int(base.weight.shape[1])
134144
except Exception as e:
135-
raise ValueError(f"Cannot infer Linear dims for LoRA injection (missing base.weight.shape): {e}") from e
145+
raise ValueError(f"Cannot infer Linear dims for LoRA injection: {e}") from e
136146

137147
# Standard LoRA init: A ~ N(0, 0.01), B = 0
138148
self.lora_A = mx.random.normal((self.r, in_dim)) * 0.01

server/training/mlx_qwen3_agent_trainer.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -531,13 +531,19 @@ def _dot_trainable(dirs: dict[str, Any]) -> float:
531531
proj_dirs_2 = None
532532

533533
# Optimizer + schedule.
534-
scheduler = optim.schedulers.join_schedules(
535-
[
536-
optim.schedulers.linear_schedule(0.0, float(lr), warmup_steps),
537-
optim.schedulers.cosine_decay(float(lr), max(1, total_steps - warmup_steps)),
538-
],
539-
[warmup_steps],
540-
)
534+
# MLX linear_schedule requires steps > 0; allow warmup_ratio=0.0 safely.
535+
if warmup_steps <= 0:
536+
scheduler = optim.schedulers.cosine_decay(float(lr), max(1, total_steps))
537+
elif warmup_steps >= total_steps:
538+
scheduler = optim.schedulers.linear_schedule(0.0, float(lr), max(1, warmup_steps))
539+
else:
540+
scheduler = optim.schedulers.join_schedules(
541+
[
542+
optim.schedulers.linear_schedule(0.0, float(lr), warmup_steps),
543+
optim.schedulers.cosine_decay(float(lr), max(1, total_steps - warmup_steps)),
544+
],
545+
[warmup_steps],
546+
)
541547
optimizer = optim.AdamW(learning_rate=scheduler, weight_decay=0.01)
542548

543549
def _grad_norm(tree: Any) -> float:

server/training/mlx_qwen3_trainer.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -548,13 +548,19 @@ def _dirs_dot(a: dict[str, Any], b: dict[str, Any]) -> float:
548548
proj_dirs_1 = None
549549
proj_dirs_2 = None
550550

551-
scheduler = optim.schedulers.join_schedules(
552-
[
553-
optim.schedulers.linear_schedule(0.0, float(lr), warmup_steps),
554-
optim.schedulers.cosine_decay(float(lr), max(1, total_steps - warmup_steps)),
555-
],
556-
[warmup_steps],
557-
)
551+
# MLX linear_schedule requires steps > 0; allow warmup_ratio=0.0 safely.
552+
if warmup_steps <= 0:
553+
scheduler = optim.schedulers.cosine_decay(float(lr), max(1, total_steps))
554+
elif warmup_steps >= total_steps:
555+
scheduler = optim.schedulers.linear_schedule(0.0, float(lr), max(1, warmup_steps))
556+
else:
557+
scheduler = optim.schedulers.join_schedules(
558+
[
559+
optim.schedulers.linear_schedule(0.0, float(lr), warmup_steps),
560+
optim.schedulers.cosine_decay(float(lr), max(1, total_steps - warmup_steps)),
561+
],
562+
[warmup_steps],
563+
)
558564
optimizer = optim.AdamW(learning_rate=scheduler, weight_decay=0.01)
559565

560566
def loss_fn(batch: list[LabeledPair]) -> Any:

web/src/components/AgentTraining/TrainingStudio.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ import { agentTrainingService, type AgentTrainRunsScope } from '@/services/Agent
1313
import type { AgentTrainMetricEvent, AgentTrainRun, AgentTrainRunMeta, AgentTrainStartRequest, ChatRequest, ChatResponse } from '@/types/generated';
1414

1515
import { NeuralVisualizer, type TelemetryPoint } from '@/components/RerankerTraining/NeuralVisualizer';
16+
import { GradientDescentViz } from '@/components/RerankerTraining/GradientDescentViz';
1617
import { StudioLogTerminal } from '@/components/RerankerTraining/StudioLogTerminal';
1718
import { RunDiff } from './RunDiff';
1819
import { RunOverview } from './RunOverview';
1920

2021
type InspectorTab = 'run-hud' | 'live-metrics' | 'overview' | 'diff' | 'config' | 'debug-prompt';
21-
type BottomTab = 'timeline' | 'logs';
22+
type BottomTab = 'timeline' | 'logs' | 'gradient';
2223
type LayoutPreset = 'balanced' | 'focus_viz' | 'focus_logs' | 'focus_inspector';
2324

2425
type StudioDockRenderers = {
@@ -1545,6 +1546,9 @@ export function TrainingStudio() {
15451546
<button className="studio-tab-btn" data-active={bottomTab === 'logs'} onClick={() => setBottomTab('logs')}>
15461547
Logs
15471548
</button>
1549+
<button className="studio-tab-btn" data-active={bottomTab === 'gradient'} onClick={() => setBottomTab('gradient')}>
1550+
Gradient Descent
1551+
</button>
15481552
{bottomTab === 'timeline' ? (
15491553
<input className="studio-search" placeholder="Filter events by type/message" value={eventQuery} onChange={(e) => setEventQuery(e.target.value)} />
15501554
) : (
@@ -1598,13 +1602,17 @@ export function TrainingStudio() {
15981602
})}
15991603
</div>
16001604
</div>
1605+
) : bottomTab === 'gradient' ? (
1606+
<div className="studio-gradient-viz" data-testid="studio-gradient-descent-viz">
1607+
<GradientDescentViz events={events} />
1608+
</div>
16011609
) : (
16021610
renderLogsBody()
16031611
)}
16041612
</div>
16051613
</section>
16061614
);
1607-
}, [bottomTab, eventQuery, eventVirtualItems, filteredEvents, renderLogsBody, setBottomTab]);
1615+
}, [bottomTab, eventQuery, eventVirtualItems, events, filteredEvents, renderLogsBody, setBottomTab]);
16081616

16091617
const dockRenderers = useMemo<StudioDockRenderers>(
16101618
() => ({
@@ -1778,4 +1786,3 @@ export function TrainingStudio() {
17781786
</section>
17791787
);
17801788
}
1781-

web/src/components/RerankerTraining/GradientDescentViz.tsx

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { useEffect, useMemo, useRef, useState } from 'react';
2-
import type { RerankerTrainMetricEvent } from '@/types/generated';
32

43
type ProjectionPoint = {
54
ts: string;
@@ -10,24 +9,39 @@ type ProjectionPoint = {
109
dy?: number;
1110
};
1211

13-
function parseProjectionPoints(events: RerankerTrainMetricEvent[]): ProjectionPoint[] {
12+
type MetricEventLike = {
13+
ts: string;
14+
step?: number | null;
15+
proj_x?: number | null;
16+
proj_y?: number | null;
17+
proj_dx?: number | null;
18+
proj_dy?: number | null;
19+
// Back-compat: some older emitters stuffed telemetry into metrics.
20+
metrics?: Record<string, unknown> | null;
21+
};
22+
23+
function parseProjectionPoints(events: MetricEventLike[]): ProjectionPoint[] {
1424
const points: ProjectionPoint[] = [];
25+
let prev: ProjectionPoint | null = null;
1526
for (const ev of events) {
16-
const m = ev.metrics as Record<string, unknown> | null | undefined;
17-
const x = m?.proj_x;
18-
const y = m?.proj_y;
27+
const m = ev.metrics;
28+
const x = typeof ev.proj_x === 'number' ? ev.proj_x : m?.proj_x;
29+
const y = typeof ev.proj_y === 'number' ? ev.proj_y : m?.proj_y;
1930
if (typeof x !== 'number' || typeof y !== 'number') continue;
2031

21-
const dx = m?.proj_dx;
22-
const dy = m?.proj_dy;
32+
const dx = typeof ev.proj_dx === 'number' ? ev.proj_dx : m?.proj_dx;
33+
const dy = typeof ev.proj_dy === 'number' ? ev.proj_dy : m?.proj_dy;
34+
const computedDx = prev ? x - prev.x : undefined;
35+
const computedDy = prev ? y - prev.y : undefined;
2336
points.push({
2437
ts: String(ev.ts),
2538
step: ev.step == null ? undefined : Number(ev.step),
2639
x,
2740
y,
28-
dx: typeof dx === 'number' ? dx : undefined,
29-
dy: typeof dy === 'number' ? dy : undefined,
41+
dx: typeof dx === 'number' ? dx : computedDx,
42+
dy: typeof dy === 'number' ? dy : computedDy,
3043
});
44+
prev = points[points.length - 1];
3145
}
3246
return points;
3347
}
@@ -36,7 +50,7 @@ function clamp(v: number, lo: number, hi: number): number {
3650
return Math.max(lo, Math.min(hi, v));
3751
}
3852

39-
export function GradientDescentViz({ events }: { events: RerankerTrainMetricEvent[] }) {
53+
export function GradientDescentViz({ events }: { events: MetricEventLike[] }) {
4054
const canvasRef = useRef<HTMLCanvasElement | null>(null);
4155
const points = useMemo(() => parseProjectionPoints(events), [events]);
4256

@@ -245,4 +259,3 @@ export function GradientDescentViz({ events }: { events: RerankerTrainMetricEven
245259
</div>
246260
);
247261
}
248-

web/src/components/RerankerTraining/TrainingStudio.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {
1818
TrainingConfig,
1919
} from '@/types/generated';
2020
import { NeuralVisualizer, type TelemetryPoint } from './NeuralVisualizer';
21+
import { GradientDescentViz } from './GradientDescentViz';
2122
import { RunDiff } from './RunDiff';
2223
import { RunOverview } from './RunOverview';
2324
import { StudioLogTerminal } from './StudioLogTerminal';
@@ -32,7 +33,7 @@ type InspectorTab =
3233
| 'config'
3334
| 'debug-score';
3435

35-
type BottomTab = 'timeline' | 'logs';
36+
type BottomTab = 'timeline' | 'logs' | 'gradient';
3637
type LayoutPreset = 'balanced' | 'focus_viz' | 'focus_logs' | 'focus_inspector';
3738

3839
type StudioDockRenderers = {
@@ -1677,6 +1678,9 @@ export function TrainingStudio() {
16771678
<button className="studio-tab-btn" data-active={bottomTab === 'logs'} onClick={() => setBottomTab('logs')}>
16781679
Logs
16791680
</button>
1681+
<button className="studio-tab-btn" data-active={bottomTab === 'gradient'} onClick={() => setBottomTab('gradient')}>
1682+
Gradient Descent
1683+
</button>
16801684
{bottomTab === 'timeline' ? (
16811685
<input
16821686
className="studio-search"
@@ -1727,6 +1731,10 @@ export function TrainingStudio() {
17271731
})}
17281732
</div>
17291733
</div>
1734+
) : bottomTab === 'gradient' ? (
1735+
<div className="studio-gradient-viz" data-testid="studio-gradient-descent-viz">
1736+
<GradientDescentViz events={events} />
1737+
</div>
17301738
) : (
17311739
renderLogsBody()
17321740
)}
@@ -1739,6 +1747,7 @@ export function TrainingStudio() {
17391747
downloadLogs,
17401748
eventQuery,
17411749
eventVirtualItems,
1750+
events,
17421751
filteredEvents,
17431752
renderLogsBody,
17441753
setBottomTab,

web/src/config/routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ export const routes: RouteConfig[] = [
108108
{ id: 'graph', title: 'Graph' },
109109
{ id: 'reranker-config', title: 'Reranker' },
110110
{ id: 'learning-ranker', title: 'Learning Ranker' },
111+
{ id: 'learning-agent', title: 'Learning Agent Studio' },
111112
{ id: 'indexing', title: 'Indexing' }
112113
],
113114
nav: { visible: true }

web/src/styles/learning-studio.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,12 @@
586586
gap: 8px;
587587
}
588588

589+
.studio-gradient-viz {
590+
min-height: 0;
591+
padding: 10px;
592+
overflow: auto;
593+
}
594+
589595
.studio-event-row {
590596
border: 1px solid var(--panel-border);
591597
border-radius: 8px;

0 commit comments

Comments
 (0)