Skip to content

Commit a5c1201

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 d1b7542 commit a5c1201

3 files changed

Lines changed: 56 additions & 4 deletions

File tree

scripts/train.py

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

src/speculators/models/metrics.py

Lines changed: 27 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]
@@ -258,6 +280,7 @@ def exp_loss_decay(pos_idx: torch.Tensor, gamma: float):
258280

259281
_LOSS_FN_MAP: dict[str, Callable[[torch.Tensor, torch.Tensor], torch.Tensor]] = {
260282
"kl_div": kl_div_loss,
283+
"rkl": reverse_kl_div_loss,
261284
"ce": ce_loss,
262285
"tv": tv_loss,
263286
"nla": neg_log_acceptance_loss,
@@ -271,9 +294,10 @@ def resolve_loss_fn(
271294
"""Resolves a loss function given its abbreviated name.
272295
273296
Args:
274-
name: ``"kl_div"`` for KL-divergence, ``"ce"`` for cross-entropy,
275-
``"tv"`` for total variation, ``"nla"`` for negative
276-
log-acceptance, or ``"lk_hybrid"`` for the adaptive KL/TV blend.
297+
name: ``"kl_div"`` for KL-divergence, ``"rkl"`` for reverse KL-divergence,
298+
``"ce"`` for cross-entropy, ``"tv"`` for total variation, ``"nla"``
299+
for negative log-acceptance, or ``"lk_hybrid"`` for the adaptive
300+
KL/TV blend.
277301
278302
Returns:
279303
The corresponding loss function.

tests/unit/models/test_metrics.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
loss_function,
1616
neg_log_acceptance_loss,
1717
resolve_loss_fn,
18+
reverse_kl_div_loss,
1819
tv_loss,
1920
)
2021

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

9596

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

0 commit comments

Comments
 (0)