42. Knowledge Distillation Loss With Temperature Scaling
Problem
Distillation trains a (usually smaller or architecturally different) student model to match a larger teacher model's output distribution, not just the ground-truth hard labels. The classic formulation blends two terms: an ordinary cross-entropy loss against the hard labels, and a KL-divergence loss between the student's and teacher's temperature-softened output distributions. Dividing both logits by a temperature T > 1 before the softmax flattens the distribution, exposing more of the teacher's relative confidence across wrong classes ("dark knowledge") instead of just its single top prediction. The soft term is rescaled by T^2 so its gradient magnitude stays comparable to the hard term's regardless of which temperature is chosen.
Implement the blended loss. This is directly the training technique behind Cartesia's own public Llamba work, distilling a Transformer teacher into a Mamba-architecture student.
Source: src/42_distillation_loss_temperature.py
def distillation_loss(
student_logits: torch.Tensor,
teacher_logits: torch.Tensor,
labels: torch.Tensor,
temperature: float = 2.0,
alpha: float = 0.5,
) -> torch.Tensor:
...
Examples:
>>> torch.manual_seed(0)
>>> student = torch.randn(4, 5)
>>> teacher = torch.randn(4, 5)
>>> labels = torch.randint(0, 5, (4,))
>>> loss = distillation_loss(student, teacher, labels)
>>> loss.shape
torch.Size([])
Step-by-Step Approach
- Compute the hard-label term first, since it's the simple part:
F.cross_entropy(student_logits, labels), completely independent of the teacher. - Soften both distributions by dividing logits by the temperature before the softmax:
teacher_logits / Tandstudent_logits / T. HigherTflattens the distribution more. - Compute the teacher's softened distribution with a plain
softmax(it's a fixed target, no gradient needs to flow through it in a real training loop). - Compute the student's softened distribution with
log_softmax, notlog(softmax(...))— this is the numerical-stability trick: taking the log of an already-computed softmax can underflow to-inffor near-zero probabilities, whilelog_softmaxcomputes log-probabilities directly and stays stable. - Combine them with
F.kl_div(student_log_probs, teacher_probs, reduction="batchmean"), matching PyTorch's expected argument order:inputis log-probabilities,targetis probabilities."batchmean"divides by batch size only, not batch times classes. - Rescale the soft loss by
T^2— softening flattens gradients too (they scale roughly as1/T), so without this the soft term's influence on training would silently shrink asTgrows, changing whatalphaeffectively means. - Blend the two terms:
alpha * hard_loss + (1 - alpha) * soft_loss. Sanity check both extremes:alpha=1should reduce to plain supervised CE (teacher ignored entirely), andalpha=0, T=1should reduce to an untempered KL divergence between the two models' raw distributions.
The key insight is that two easy-to-miss details, log_softmax instead of log(softmax()) for stability, and the T^2 rescale for gradient-magnitude consistency, are exactly the parts that separate a "looks right" implementation from one that actually reproduces the textbook Hinton distillation loss.
Reference solution
def distillation_loss(student_logits, teacher_logits, labels, temperature=2.0, alpha=0.5):
# Hard term: ordinary supervised cross-entropy against ground-truth labels.
hard_loss = F.cross_entropy(student_logits, labels)
# Soft term: KL-divergence between temperature-softened teacher and
# student distributions. Dividing by T > 1 flattens both softmaxes,
# exposing the teacher's relative confidence across *wrong* classes.
#
# NUMERICAL-STABILITY TRICK: use log_softmax for the student side (not
# log(softmax(...))) -- computing softmax then taking its log can
# underflow to log(0) = -inf; log_softmax stays numerically stable.
soft_teacher = F.softmax(teacher_logits / temperature, dim=-1)
soft_student_log = F.log_softmax(student_logits / temperature, dim=-1)
# F.kl_div expects (input=log-probs, target=probs); "batchmean" divides
# the summed-over-classes KL by the batch size only.
soft_loss = F.kl_div(soft_student_log, soft_teacher, reduction="batchmean")
# T^2 RESCALE: keeps the soft term's gradient magnitude comparable to
# the hard term's across different temperature choices.
soft_loss = soft_loss * (temperature ** 2)
return alpha * hard_loss + (1 - alpha) * soft_loss
Key Functions & Tricks
F.log_softmaxvs.torch.log(F.softmax(...))— the numerical-stability fix; the fused version avoids the intermediate underflow.F.kl_div(input, target, reduction="batchmean")— note the non-obvious argument order (log-probs first, probs second) and that"batchmean"differs from the (deprecated) default"mean"reduction.temperature ** 2rescale — keepsalpha's meaning stable across different temperature choices.logits / temperaturebefore softmax — the softening step; note it's applied to both teacher and student consistently.- Independent test oracle via a manual elementwise KL sum (
(p * (log(p) - log_q)).sum(-1).mean()) — validates the loss's math without depending on the sameF.kl_divcall path the solution uses.
How to Recognize This Pattern
The signal is "train a smaller/faster model to match a larger model's soft outputs" — distillation shows up anywhere inference cost matters more than training cost, which is exactly the tradeoff behind Cartesia's own published work distilling a Transformer into a recurrent architecture for real-time voice inference. Common variations ask for distillation on intermediate hidden states or attention maps instead of just final logits, or ask what changes if the teacher's logits should also be detached from the autograd graph (they always should be — gradients never need to flow into a frozen teacher). The most common pitfalls are swapping the F.kl_div argument order (probs where log-probs are expected, or vice versa), forgetting the T^2 rescale, and using log(softmax(x)) instead of the fused, numerically stable log_softmax(x).