Skip to content

Commit b6b6665

Browse files
feat(training): support configurable loss fn via training args
Signed-off-by: Christina Xu <chrxu@redhat.com>
1 parent 8e023e0 commit b6b6665

9 files changed

Lines changed: 161 additions & 15 deletions

File tree

scripts/train.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
DEFAULT_REQUEST_TIMEOUT,
1717
)
1818
from speculators.model import SpeculatorModel
19+
from speculators.models.metrics import resolve_loss_fn
1920
from speculators.models.eagle3.data import shift_batch
2021
from speculators.train.data import (
2122
ArrowDataset,
@@ -568,6 +569,17 @@ def parse_args():
568569
parser.add_argument("--mask-token-id", type=int, default=None)
569570
parser.add_argument("--ttt-steps", type=int, default=3)
570571
parser.add_argument("--ttt-step-loss-decay", type=float, default=1.0)
572+
parser.add_argument(
573+
"--loss-fn",
574+
type=str,
575+
default=None,
576+
choices=["kl", "ce"],
577+
help=(
578+
"Loss function used during draft model training. "
579+
"'kl' = KL divergence. "
580+
"'ce' = cross-entropy. "
581+
),
582+
)
571583
parser.add_argument(
572584
"--seed", type=int, default=42, help="Random seed for reproducibility"
573585
)
@@ -673,7 +685,11 @@ def parse_args():
673685
parser.add_argument("--scheduler-warmup-steps", type=int, default=None)
674686
parser.add_argument("--scheduler-total-steps", type=int, default=None)
675687
parser.add_argument("--scheduler-num-cosine-cycles", type=float, default=0.5)
676-
return parser.parse_args()
688+
689+
args = parser.parse_args()
690+
if args.loss_fn is not None:
691+
resolve_loss_fn(args.loss_fn)
692+
return args
677693

678694

679695
if __name__ == "__main__":

src/speculators/models/dflash/core.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from speculators.models.dflash import DFlashSpeculatorConfig
1414
from speculators.models.dflash.attention import create_anchor_block_mask_mod
1515
from speculators.models.dflash.metrics import compute_metrics
16+
from speculators.models.metrics import ce_loss, resolve_loss_fn
1617
from speculators.models.dflash.model_definitions import Qwen3DFlashDecoderLayer
1718
from speculators.models.dflash.utils import (
1819
get_base_indices_for_anchored_blocks,
@@ -162,7 +163,7 @@ def from_training_args(
162163
return model
163164

164165
@staticmethod
165-
def get_trainer_kwargs(**kwargs) -> tuple[dict, dict]: # noqa: ARG004
166+
def get_trainer_kwargs(**kwargs) -> tuple[dict, dict]: # noqa: ARG004
166167
"""Get training and validation kwargs for DFlash.
167168
168169
Args:
@@ -171,8 +172,9 @@ def get_trainer_kwargs(**kwargs) -> tuple[dict, dict]: # noqa: ARG004
171172
Returns:
172173
Tuple of (train_call_kwargs, val_call_kwargs)
173174
"""
174-
train_kwargs: dict[str, Any] = {}
175-
val_kwargs: dict[str, Any] = {}
175+
loss_fn = resolve_loss_fn(kwargs.get("loss_fn") or "ce")
176+
train_kwargs: dict[str, Any] = {"loss_fn": loss_fn}
177+
val_kwargs: dict[str, Any] = {"loss_fn": loss_fn}
176178
return train_kwargs, val_kwargs
177179

178180
@property
@@ -194,6 +196,7 @@ def forward(
194196
verifier_last_hidden_states: torch.Tensor, # shape: [1, total_seq_len, hidden_size] # noqa: E501
195197
lengths: torch.Tensor | None = None, # shape: [batch_size]
196198
position_ids: torch.Tensor | None = None, # shape: [1, total_seq_len]
199+
loss_fn=ce_loss,
197200
**kwargs,
198201
):
199202
device = hidden_states.device
@@ -293,7 +296,7 @@ def forward(
293296

294297
aligned_loss_mask[:, :: self.block_size] = 0
295298
loss, metrics = compute_metrics(
296-
logits, targets, aligned_loss_mask, self.block_size
299+
logits, targets, aligned_loss_mask, self.block_size, loss_fn=loss_fn
297300
)
298301
draft_tokens = torch.argmax(logits, dim=-1)
299302

src/speculators/models/dflash/metrics.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Metrics and loss functions for DFlash draft model."""
22

3+
from collections.abc import Callable
34
from functools import partial
45
from typing import Any
56

@@ -19,6 +20,7 @@ def compute_metrics(
1920
loss_mask: torch.Tensor, # shape: [1, num_anchors*block_size]
2021
block_size: int = 1,
2122
gamma: float = 4.0,
23+
loss_fn: Callable[[torch.Tensor, torch.Tensor], torch.Tensor] = ce_loss,
2224
) -> tuple[torch.Tensor, dict]:
2325
"""Compute loss and accuracy metrics for draft model predictions.
2426
@@ -28,6 +30,7 @@ def compute_metrics(
2830
loss_mask: Binary mask [1, T]
2931
block_size: Block size for per-position metrics
3032
gamma: Temperature for exponential decay in loss weighting
33+
loss_fn: Loss function
3134
3235
Returns:
3336
Tuple of (loss, metrics_dict) where metrics_dict contains:
@@ -44,7 +47,7 @@ def compute_metrics(
4447
targets,
4548
loss_mask,
4649
pos_idx,
47-
loss_fn=ce_loss,
50+
loss_fn=loss_fn,
4851
decay_fn=partial(dflash_loss_decay, gamma=gamma),
4952
)
5053

src/speculators/models/eagle3/core.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
extend_mask_for_draft_tokens,
1414
)
1515
from speculators.models.eagle3.metrics import compute_metrics
16+
from speculators.models.metrics import kl_div_loss, resolve_loss_fn
1617
from speculators.models.eagle3.model_definitions import model_classes
1718
from speculators.models.utils import resolve_target_layer_ids
1819
from speculators.proposals.greedy import GreedyTokenProposalConfig
@@ -130,6 +131,7 @@ def forward( # noqa: C901
130131
ttt_steps: int = 3,
131132
ttt_step_loss_decay: float = 1.0,
132133
use_off_policy_tokens: bool = False,
134+
loss_fn=kl_div_loss,
133135
**kwargs,
134136
):
135137
device = hidden_states.device
@@ -222,6 +224,7 @@ def forward( # noqa: C901
222224
prev_correct,
223225
ttt_step,
224226
ttt_step_loss_decay,
227+
loss_fn=loss_fn,
225228
)
226229
loss += s_loss
227230
metrics.update(s_metrics)
@@ -324,14 +327,17 @@ def get_trainer_kwargs(**kwargs) -> tuple[dict, dict]:
324327
Returns:
325328
Tuple of (train_call_kwargs, val_call_kwargs)
326329
"""
330+
loss_fn = resolve_loss_fn(kwargs.get("loss_fn") or "kl")
327331
train_kwargs = {
328332
"use_off_policy_tokens": kwargs["use_off_policy_tokens"],
329333
"ttt_steps": kwargs["ttt_steps"],
330334
"ttt_step_loss_decay": kwargs["ttt_step_loss_decay"],
335+
"loss_fn": loss_fn,
331336
}
332337
val_kwargs = {
333338
"use_off_policy_tokens": False,
334339
"ttt_steps": kwargs["ttt_steps"],
335340
"ttt_step_loss_decay": kwargs["ttt_step_loss_decay"],
341+
"loss_fn": loss_fn,
336342
}
337343
return train_kwargs, val_kwargs

src/speculators/models/eagle3/metrics.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Metrics and loss functions for Eagle3 draft model."""
22

3+
from collections.abc import Callable
34
from functools import partial
45

56
import torch
@@ -54,6 +55,7 @@ def compute_metrics(
5455
prev_correct: torch.Tensor | None,
5556
ttt_step: int,
5657
ttt_step_loss_decay: float,
58+
loss_fn: Callable[[torch.Tensor, torch.Tensor], torch.Tensor] = kl_div_loss,
5759
) -> tuple[torch.Tensor, dict]:
5860
"""Compute metrics for a given ttt_step.
5961
@@ -64,6 +66,7 @@ def compute_metrics(
6466
prev_correct: The previous correct predictions for the current ttt_step.
6567
ttt_step: The current ttt_step.
6668
ttt_step_loss_decay: The loss decay for the current ttt_step.
69+
loss_fn: Loss function.
6770
6871
Effects:
6972
Modifies prev_correct in place.
@@ -88,7 +91,7 @@ def compute_metrics(
8891
s_targets,
8992
s_loss_mask,
9093
pos_idx,
91-
loss_fn=kl_div_loss,
94+
loss_fn=loss_fn,
9295
decay_fn=partial(exp_loss_decay, gamma=ttt_step_loss_decay),
9396
)
9497

src/speculators/models/metrics.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,31 @@ def exp_loss_decay(pos_idx: torch.Tensor, gamma: float):
153153
return gamma**pos_idx
154154

155155

156+
def resolve_loss_fn(
157+
name: str,
158+
) -> "Callable[[torch.Tensor, torch.Tensor], torch.Tensor]":
159+
"""Resolves a loss function given its abbreviated name.
160+
161+
Args:
162+
name: ``"kl"`` for KL-divergence or ``"ce"`` for cross-entropy.
163+
164+
Returns:
165+
The corresponding loss function.
166+
167+
Raises:
168+
ValueError: If *name* is not a recognised loss function.
169+
"""
170+
loss_fn_map: dict[str, Callable[[torch.Tensor, torch.Tensor], torch.Tensor]] = {
171+
"kl": kl_div_loss,
172+
"ce": ce_loss,
173+
}
174+
if name not in loss_fn_map:
175+
raise ValueError(
176+
f"Unknown loss function '{name}'. Choose from: {sorted(loss_fn_map.keys())}"
177+
)
178+
return loss_fn_map[name]
179+
180+
156181
def loss_function(
157182
logits: torch.Tensor, # shape: [1, seq_len, draft_vocab_size]
158183
targets: torch.Tensor, # shape: [1, seq_len, draft_vocab_size]

src/speculators/models/peagle/core.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from speculators.config import SpeculatorsConfig, VerifierConfig
1010
from speculators.model import SpeculatorModel
1111
from speculators.models.eagle3.core import Eagle3DraftModel, conditional_torch_compile
12+
from speculators.models.metrics import kl_div_loss, resolve_loss_fn
1213
from speculators.models.peagle.attention import create_peagle_mask_mod
1314
from speculators.models.peagle.config import PEagleSpeculatorConfig
1415
from speculators.models.peagle.data import generate_cod_sample_indices
@@ -55,6 +56,7 @@ def forward(
5556
position_ids: torch.Tensor | None = None,
5657
loss_mask: torch.Tensor | None = None,
5758
verifier_last_hidden_states: torch.Tensor | None = None,
59+
loss_fn=kl_div_loss,
5860
**kwargs,
5961
):
6062
"""
@@ -172,6 +174,7 @@ def forward(
172174
anchor_pos=anchor_pos,
173175
depth=depth,
174176
num_depths=self.num_depths,
177+
loss_fn=loss_fn,
175178
)
176179

177180
return None, loss, metrics
@@ -237,17 +240,15 @@ def from_training_args(
237240
return model
238241

239242
@staticmethod
240-
def get_trainer_kwargs(**kwargs) -> tuple[dict, dict]: # noqa: ARG004
243+
def get_trainer_kwargs(**kwargs) -> tuple[dict, dict]:
241244
"""
242245
Get training and validation kwargs for P-EAGLE.
243246
244-
P-EAGLE doesn't need extra kwargs for forward() - all parameters
245-
are handled in the forward method (num_depths, down_sample_ratio, etc.)
246-
247247
Args:
248-
**kwargs: Training arguments (unused)
248+
**kwargs: Training arguments
249249
250250
Returns:
251-
Tuple of (train_call_kwargs, val_call_kwargs) - both empty for P-EAGLE
251+
Tuple of (train_call_kwargs, val_call_kwargs)
252252
"""
253-
return {}, {}
253+
loss_fn = resolve_loss_fn(kwargs.get("loss_fn") or "kl")
254+
return {"loss_fn": loss_fn}, {"loss_fn": loss_fn}

src/speculators/models/peagle/metrics.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Metrics and loss functions for P-EAGLE draft model."""
22

3+
from collections.abc import Callable
34
from typing import Any
45

56
import torch
@@ -18,6 +19,7 @@ def compute_metrics(
1819
anchor_pos: torch.Tensor,
1920
depth: torch.Tensor,
2021
num_depths: int,
22+
loss_fn: Callable[[torch.Tensor, torch.Tensor], torch.Tensor] = kl_div_loss,
2123
) -> tuple[torch.Tensor, dict[str, Any]]:
2224
"""Compute loss and accuracy metrics for P-EAGLE predictions.
2325
@@ -29,6 +31,7 @@ def compute_metrics(
2931
sampling chain started from [total_sampled]
3032
depth: Which COD sampling round each element belongs to [total_sampled]
3133
num_depths: Number of parallel depths
34+
loss_fn: Loss function.
3235
3336
Returns:
3437
Tuple of (loss, metrics_dict)
@@ -41,7 +44,7 @@ def compute_metrics(
4144
sampled_loss_mask = loss_mask[:, orig_positions] # [1, total_sampled]
4245

4346
loss = loss_function(
44-
logits, targets, sampled_loss_mask, depth.unsqueeze(0), loss_fn=kl_div_loss
47+
logits, targets, sampled_loss_mask, depth.unsqueeze(0), loss_fn=loss_fn
4548
)
4649

4750
with torch.no_grad():

tests/unit/train/test_cli_args.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""Tests for CLI arguments."""
2+
3+
import pytest
4+
5+
from scripts.train import parse_args
6+
from speculators.models.dflash.core import DFlashDraftModel
7+
from speculators.models.eagle3.core import Eagle3DraftModel
8+
from speculators.models.metrics import ce_loss, kl_div_loss
9+
from speculators.models.peagle.core import PEagleDraftModel
10+
11+
12+
def _parse(monkeypatch, extra: list[str]):
13+
monkeypatch.setattr(
14+
"sys.argv", ["train.py", "--verifier-name-or-path", "dummy"] + extra
15+
)
16+
return parse_args()
17+
18+
19+
# ---------------------------------------------------------------------------
20+
# Ensure CLI args flow correctly through vars(args) into get_trainer_kwargs
21+
# ---------------------------------------------------------------------------
22+
23+
class TestDFlashLossFnCLI:
24+
"""DFlash: verify --loss-fn flows correctly through vars(args) into get_trainer_kwargs."""
25+
26+
def test_default_args_use_ce(self, monkeypatch):
27+
args = _parse(monkeypatch, [])
28+
train_kw, val_kw = DFlashDraftModel.get_trainer_kwargs(**vars(args))
29+
assert train_kw["loss_fn"] is ce_loss
30+
assert val_kw["loss_fn"] is ce_loss
31+
32+
def test_loss_fn_kl_overrides_default(self, monkeypatch):
33+
args = _parse(monkeypatch, ["--loss-fn", "kl"])
34+
train_kw, val_kw = DFlashDraftModel.get_trainer_kwargs(**vars(args))
35+
assert train_kw["loss_fn"] is kl_div_loss
36+
assert val_kw["loss_fn"] is kl_div_loss
37+
38+
def test_loss_fn_ce_explicit(self, monkeypatch):
39+
args = _parse(monkeypatch, ["--loss-fn", "ce"])
40+
train_kw, val_kw = DFlashDraftModel.get_trainer_kwargs(**vars(args))
41+
assert train_kw["loss_fn"] is ce_loss
42+
assert val_kw["loss_fn"] is ce_loss
43+
44+
45+
class TestEagle3LossFnCLI:
46+
"""Eagle3: verify --loss-fn flows correctly through vars(args) into get_trainer_kwargs."""
47+
48+
def test_default_args_use_kl(self, monkeypatch):
49+
args = _parse(monkeypatch, [])
50+
train_kw, val_kw = Eagle3DraftModel.get_trainer_kwargs(**vars(args))
51+
assert train_kw["loss_fn"] is kl_div_loss
52+
assert val_kw["loss_fn"] is kl_div_loss
53+
54+
def test_loss_fn_ce_overrides_default(self, monkeypatch):
55+
args = _parse(monkeypatch, ["--loss-fn", "ce"])
56+
train_kw, val_kw = Eagle3DraftModel.get_trainer_kwargs(**vars(args))
57+
assert train_kw["loss_fn"] is ce_loss
58+
assert val_kw["loss_fn"] is ce_loss
59+
60+
def test_loss_fn_kl_explicit(self, monkeypatch):
61+
args = _parse(monkeypatch, ["--loss-fn", "kl"])
62+
train_kw, val_kw = Eagle3DraftModel.get_trainer_kwargs(**vars(args))
63+
assert train_kw["loss_fn"] is kl_div_loss
64+
assert val_kw["loss_fn"] is kl_div_loss
65+
66+
67+
class TestPEagleLossFnCLI:
68+
"""PEagle: verify --loss-fn flows correctly through vars(args) into get_trainer_kwargs."""
69+
70+
def test_default_args_use_kl(self, monkeypatch):
71+
args = _parse(monkeypatch, [])
72+
train_kw, val_kw = PEagleDraftModel.get_trainer_kwargs(**vars(args))
73+
assert train_kw["loss_fn"] is kl_div_loss
74+
assert val_kw["loss_fn"] is kl_div_loss
75+
76+
def test_loss_fn_ce_overrides_default(self, monkeypatch):
77+
args = _parse(monkeypatch, ["--loss-fn", "ce"])
78+
train_kw, val_kw = PEagleDraftModel.get_trainer_kwargs(**vars(args))
79+
assert train_kw["loss_fn"] is ce_loss
80+
assert val_kw["loss_fn"] is ce_loss
81+
82+
def test_loss_fn_kl_explicit(self, monkeypatch):
83+
args = _parse(monkeypatch, ["--loss-fn", "kl"])
84+
train_kw, val_kw = PEagleDraftModel.get_trainer_kwargs(**vars(args))
85+
assert train_kw["loss_fn"] is kl_div_loss
86+
assert val_kw["loss_fn"] is kl_div_loss

0 commit comments

Comments
 (0)