Skip to content

Commit 74d1ffc

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

9 files changed

Lines changed: 136 additions & 17 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 & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, ClassVar
1+
from typing import ClassVar
22

33
import torch
44
from torch import nn
@@ -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 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]:
166167
"""Get training and validation kwargs for DFlash.
167168
168169
Args:
@@ -171,9 +172,8 @@ 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] = {}
176-
return train_kwargs, val_kwargs
175+
loss_fn = resolve_loss_fn(kwargs["loss_fn"])
176+
return {"loss_fn": loss_fn}, {"loss_fn": loss_fn}
177177

178178
@property
179179
def mask_token_id(self) -> int:
@@ -194,6 +194,7 @@ def forward(
194194
verifier_last_hidden_states: torch.Tensor, # shape: [1, total_seq_len, hidden_size] # noqa: E501
195195
lengths: torch.Tensor | None = None, # shape: [batch_size]
196196
position_ids: torch.Tensor | None = None, # shape: [1, total_seq_len]
197+
loss_fn=None,
197198
**kwargs,
198199
):
199200
device = hidden_states.device
@@ -293,7 +294,7 @@ def forward(
293294

294295
aligned_loss_mask[:, :: self.block_size] = 0
295296
loss, metrics = compute_metrics(
296-
logits, targets, aligned_loss_mask, self.block_size
297+
logits, targets, aligned_loss_mask, self.block_size, loss_fn=loss_fn
297298
)
298299
draft_tokens = torch.argmax(logits, dim=-1)
299300

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 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=None,
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: 5 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,13 +66,15 @@ 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.
7073
7174
Returns:
7275
Loss value and metrics dictionary.
7376
"""
77+
loss_fn = loss_fn or kl_div_loss
7478
s_logits, s_targets, s_loss_mask, s_prev_correct = align_for_step(
7579
logits, targets, loss_mask, prev_correct, ttt_step
7680
)
@@ -88,7 +92,7 @@ def compute_metrics(
8892
s_targets,
8993
s_loss_mask,
9094
pos_idx,
91-
loss_fn=kl_div_loss,
95+
loss_fn=loss_fn,
9296
decay_fn=partial(exp_loss_decay, gamma=ttt_step_loss_decay),
9397
)
9498

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
@@ -12,6 +12,7 @@
1212
from speculators.models.peagle.attention import create_peagle_mask_mod
1313
from speculators.models.peagle.config import PEagleSpeculatorConfig
1414
from speculators.models.peagle.data import generate_cod_sample_indices
15+
from speculators.models.metrics import resolve_loss_fn
1516
from speculators.models.peagle.metrics import compute_metrics
1617
from speculators.models.utils import resolve_target_layer_ids
1718
from speculators.proposals.greedy import GreedyTokenProposalConfig
@@ -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=None,
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: 5 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,10 +31,12 @@ 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)
3538
"""
39+
loss_fn = loss_fn or kl_div_loss
3640
device = logits.device
3741

3842
# TODO: batch size is always 1 for P-EAGLE; unsqueeze is only to match the
@@ -41,7 +45,7 @@ def compute_metrics(
4145
sampled_loss_mask = loss_mask[:, orig_positions] # [1, total_sampled]
4246

4347
loss = loss_function(
44-
logits, targets, sampled_loss_mask, depth.unsqueeze(0), loss_fn=kl_div_loss
48+
logits, targets, sampled_loss_mask, depth.unsqueeze(0), loss_fn=loss_fn
4549
)
4650

4751
with torch.no_grad():

tests/unit/train/test_cli_args.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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+
def test_default_uses_kl(self, monkeypatch):
23+
args = _parse(monkeypatch, [])
24+
train_kw, val_kw = DFlashDraftModel.get_trainer_kwargs(**vars(args))
25+
assert train_kw["loss_fn"] is kl_div_loss
26+
assert val_kw["loss_fn"] is kl_div_loss
27+
28+
def test_explicit_ce(self, monkeypatch):
29+
args = _parse(monkeypatch, ["--loss-fn", "ce"])
30+
train_kw, val_kw = DFlashDraftModel.get_trainer_kwargs(**vars(args))
31+
assert train_kw["loss_fn"] is ce_loss
32+
assert val_kw["loss_fn"] is ce_loss
33+
34+
35+
class TestEagle3LossFnCLI:
36+
def test_default_uses_kl(self, monkeypatch):
37+
args = _parse(monkeypatch, [])
38+
train_kw, val_kw = Eagle3DraftModel.get_trainer_kwargs(**vars(args))
39+
assert train_kw["loss_fn"] is kl_div_loss
40+
assert val_kw["loss_fn"] is kl_div_loss
41+
42+
def test_explicit_ce(self, monkeypatch):
43+
args = _parse(monkeypatch, ["--loss-fn", "ce"])
44+
train_kw, val_kw = Eagle3DraftModel.get_trainer_kwargs(**vars(args))
45+
assert train_kw["loss_fn"] is ce_loss
46+
assert val_kw["loss_fn"] is ce_loss
47+
48+
49+
class TestPEagleLossFnCLI:
50+
def test_default_uses_kl(self, monkeypatch):
51+
args = _parse(monkeypatch, [])
52+
train_kw, val_kw = PEagleDraftModel.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_explicit_ce(self, monkeypatch):
57+
args = _parse(monkeypatch, ["--loss-fn", "ce"])
58+
train_kw, val_kw = PEagleDraftModel.get_trainer_kwargs(**vars(args))
59+
assert train_kw["loss_fn"] is ce_loss
60+
assert val_kw["loss_fn"] is ce_loss

0 commit comments

Comments
 (0)