Skip to content

Commit 5e73609

Browse files
committed
Add reverse KL divergence (RKL) loss for speculative decoding
Signed-off-by: Guan-Ming (Wesley) Chiu <105915352+guan404ming@users.noreply.github.com>
1 parent ff71b1e commit 5e73609

3 files changed

Lines changed: 55 additions & 4 deletions

File tree

scripts/train.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -863,7 +863,7 @@ def parse_args():
863863
default="kl_div",
864864
help=(
865865
"Loss function specification. Pass a name for a single loss "
866-
"(kl_div, ce, tv, nla) or a JSON dict for a weighted combination, "
866+
"(kl_div, rkl, ce, tv, nla) or a JSON dict for a weighted combination, "
867867
'e.g. \'{"ce": 0.1, "tv": 0.9}\'.'
868868
),
869869
)

src/speculators/models/metrics.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,28 @@ def kl_div_loss(
9898
return elementwise_loss # noqa: RET504
9999

100100

101+
def reverse_kl_div_loss(
102+
logits: torch.Tensor, # shape: [1, seq_len, draft_vocab_size]
103+
targets: torch.Tensor, # shape: [1, seq_len, draft_vocab_size]
104+
):
105+
"""Compute per-position reverse KL divergence from draft logits to target logits.
106+
107+
Args:
108+
logits: Draft model logits (log-softmax applied internally).
109+
targets: Target model logits (log-softmax applied internally).
110+
111+
Returns:
112+
Per-position reverse KL divergence with shape [1, seq_len].
113+
"""
114+
draft_logq = torch.nn.functional.log_softmax(logits, dim=-1)
115+
target_logp = torch.nn.functional.log_softmax(targets, dim=-1)
116+
elementwise_loss = torch.nn.functional.kl_div(
117+
target_logp, draft_logq, reduction="none", log_target=True
118+
).sum(dim=-1) # shape: [1, seq_len]
119+
120+
return elementwise_loss # noqa: RET504
121+
122+
101123
def ce_loss(
102124
logits: torch.Tensor, # shape: [1, seq_len, draft_vocab_size]
103125
targets: torch.Tensor, # shape: [1, seq_len, draft_vocab_size]
@@ -216,6 +238,7 @@ def exp_loss_decay(pos_idx: torch.Tensor, gamma: float):
216238

217239
_LOSS_FN_MAP: dict[str, Callable[[torch.Tensor, torch.Tensor], torch.Tensor]] = {
218240
"kl_div": kl_div_loss,
241+
"rkl": reverse_kl_div_loss,
219242
"ce": ce_loss,
220243
"tv": tv_loss,
221244
"nla": neg_log_acceptance_loss,
@@ -228,9 +251,9 @@ def resolve_loss_fn(
228251
"""Resolves a loss function given its abbreviated name.
229252
230253
Args:
231-
name: ``"kl_div"`` for KL-divergence, ``"ce"`` for cross-entropy,
232-
``"tv"`` for total variation, or ``"nla"`` for negative
233-
log-acceptance.
254+
name: ``"kl_div"`` for KL-divergence, ``"rkl"`` for reverse KL-divergence,
255+
``"ce"`` for cross-entropy, ``"tv"`` for total variation, or ``"nla"``
256+
for negative log-acceptance.
234257
235258
Returns:
236259
The corresponding loss function.

tests/unit/models/test_metrics.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
loss_function,
1515
neg_log_acceptance_loss,
1616
resolve_loss_fn,
17+
reverse_kl_div_loss,
1718
tv_loss,
1819
)
1920

@@ -92,6 +93,33 @@ def test_identical_zero_and_random_nonnegative(self):
9293
assert (loss_random >= -1e-6).all()
9394

9495

96+
class TestReverseKLDivLoss:
97+
def test_identical_zero_and_random_nonnegative(self):
98+
"""Reverse KL of identical distributions is ~0; random inputs are >= 0."""
99+
x = torch.randn(1, 4, 8)
100+
loss_identical = reverse_kl_div_loss(x, x)
101+
assert loss_identical.sum().item() == pytest.approx(0.0, abs=1e-4)
102+
103+
torch.manual_seed(0)
104+
loss_random = reverse_kl_div_loss(torch.randn(1, 4, 8), torch.randn(1, 4, 8))
105+
assert (loss_random >= -1e-6).all()
106+
107+
def test_equals_forward_kl_with_swapped_args(self):
108+
"""KL(q||p) equals forward KL with draft and target swapped."""
109+
torch.manual_seed(0)
110+
logits = torch.randn(1, 4, 8)
111+
targets = torch.randn(1, 4, 8)
112+
assert torch.allclose(
113+
reverse_kl_div_loss(logits, targets),
114+
kl_div_loss(targets, logits),
115+
atol=1e-6,
116+
)
117+
118+
def test_resolve_rkl(self):
119+
"""resolve_loss_fn maps 'rkl' to reverse_kl_div_loss."""
120+
assert resolve_loss_fn("rkl") is reverse_kl_div_loss
121+
122+
95123
class TestTVLoss:
96124
def test_identical_is_zero(self):
97125
"""TV distance between identical distributions is ~0."""

0 commit comments

Comments
 (0)