Skip to content

Commit a87277e

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

9 files changed

Lines changed: 164 additions & 15 deletions

File tree

scripts/train.py

Lines changed: 16 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,
@@ -574,6 +575,17 @@ def parse_args():
574575
parser.add_argument("--mask-token-id", type=int, default=None)
575576
parser.add_argument("--ttt-steps", type=int, default=3)
576577
parser.add_argument("--ttt-step-loss-decay", type=float, default=1.0)
578+
parser.add_argument(
579+
"--loss-fn",
580+
type=str,
581+
default="kl",
582+
choices=["kl", "ce"],
583+
help=(
584+
"Loss function used during draft model training. "
585+
"'kl' = KL divergence (default). "
586+
"'ce' = cross-entropy."
587+
),
588+
)
577589
parser.add_argument(
578590
"--seed", type=int, default=42, help="Random seed for reproducibility"
579591
)
@@ -679,7 +691,10 @@ def parse_args():
679691
parser.add_argument("--scheduler-warmup-steps", type=int, default=None)
680692
parser.add_argument("--scheduler-total-steps", type=int, default=None)
681693
parser.add_argument("--scheduler-num-cosine-cycles", type=float, default=0.5)
682-
return parser.parse_args()
694+
695+
args = parser.parse_args()
696+
resolve_loss_fn(args.loss_fn)
697+
return args
683698

684699

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

0 commit comments

Comments
 (0)