Offline Logits Knowledge Distillation#
Offline logits knowledge distillation (KD) trains a student model against a teacher’s cached top-K token log-probabilities instead of running the teacher live. A separate teacher run saves top-K log-probs to disk once; any number of later student runs stream them back and add a forward-KL loss term, without ever loading teacher weights. This decouples teacher and student compute (different job, different cluster, different time) and removes the teacher’s memory/compute footprint from the student job entirely.
This is a different mechanism from ModelOpt’s knowledge distillation, which runs teacher and student together in the same process via module-swap. Use offline logits KD when the teacher is too large/expensive to co-locate with the student, or when you want to distill from the same cached teacher run into several independent student configurations.
Implemented in megatron/training/distillation/:
logits_saver.py—LogitsSaverHooks, attached to the teacher’s output layer.cached_logits_loss.py—LossFuncCallable(student-side loss),CachedLogitsKDLoss,StudentLogitsCapture.utils.py— shared tar/storage format, index bit-packing, and the DP/MBS/GBS resharding plan.
How It Works#
Saving (teacher run): pass --logits-save-dir. LogitsSaverHooks attaches a
forward hook to the model’s output layer on the last pipeline stage
(megatron/training/training.py, gated on args.logits_save_dir is not None). Each
microbatch’s logits are converted to top-K log-probs (TP-aware: local top-K is computed
per rank first, then gathered to TP rank 0 for the global top-K, avoiding a
full-vocab-sized tensor), optionally narrowed further with top-P nucleus masking, and
buffered until the whole iteration is collected. Buffered iterations are flushed
asynchronously as .tar shards (zstd-compressed, one shard per DP rank per flush)
through Megatron’s async checkpoint queue: the flush is enqueued from
save_checkpoint() (megatron/training/checkpointing.py), so buffered logits are
written to disk at the same cadence as regular checkpoints, i.e. every
--save-interval iterations — not every iteration. This requires --async-save
and --use-persistent-ckpt-worker to be set, since the write itself piggybacks on
the async checkpoint worker rather than running inline. A dedicated teacher-dump run
should also pass --freeze-all-layers; without it, the model keeps training (LM loss
computed, gradients applied) while logits are saved on the side.
Loading (student run): pass --logits-load-dir. StudentLogitsCapture attaches a
capture hook to the student’s output layer; LossFuncCallable (constructed once per
process, wired into loss_func in pretrain_gpt.py / pretrain_hybrid.py) streams the
matching tar shard(s) for the current DP rank and training iteration via a
torch.utils.data.DataLoader (pin_memory=True, decode overlapped with GPU compute),
and computes the sparse forward KL divergence KL(teacher ‖ student) between the
teacher’s top-K positions and the student’s log-probs at those same vocab indices. The
total loss is alpha * kd_loss + (1 - alpha) * lm_loss.
Both hooks are attached automatically based on the CLI flags below — no application
code changes are needed beyond invoking pretrain_gpt.py / pretrain_hybrid.py with
the relevant --logits-save-* or --logits-load-* flags.
On-Disk Format#
Shards are named dp{D}__{start}-{end}.tar, keyed by the global sample index range
[start, end) they cover (not by iteration number), so shard identity is independent of
global batch size. Each tar’s first member is a JSON metadata blob (_meta.json,
including a dataset-identity hash used to catch teacher/student data mismatches) followed
by one compressed payload per saved iteration. Vocab indices are stored as 16 low bits
(uint16) plus a separately bit-packed 17th bit (1 bit/element via
numpy.packbits/unpackbits, not torch.bool’s 1 byte/element), reconstructed as
(bit_17 << 16) | low_16_bits.
Superseded shards from a crash-and-resume are quarantined (renamed with a .stale
suffix, not deleted) rather than overwritten in place, so a crash mid-write never leaves
two overlapping live shards for the same range.
Resharding#
The student run’s parallelism does not need to match the teacher run’s:
Data parallelism may differ freely between save and load, including non-power-of-2 ratios (e.g. 6 saved DP ranks → 4 loaded DP ranks).
Micro-batch size and global batch size may each differ from the teacher run, provided the new value is an integer multiple (in either direction) of the saved value.
Context parallelism is CP-agnostic: shards store the full sequence, and each rank slices out its own CP zigzag chunk locally, so CP size can change freely as long as sequence length stays divisible by
2 * context_parallel_size.
A LogprobsReshardPlan (megatron/training/distillation/utils.py) computes, for each
load-side microbatch, which saved DP-rank shard(s) and row ranges to read.
Assumptions and Restrictions#
The student run must use the same random seed and data pipeline as the teacher run that produced the cached log-probs, so the global sample ordering matches.
Teacher data must be saved with payload
format_version >= 2for full DP/MBS/GBS resharding support. Olderformat_version == 1shards are still readable but are restricted to DP-only resharding, via a legacy reader slated for removal once existing caches are regenerated.The loader enforces sequential, non-overlapping shard consumption per saved DP rank, and checks every iteration that the cached teacher data starts at the same global sample as the student’s batch (
consumed_train_samples). Teacher data that is behind (duplicate/overlapping shards) is skipped with a warning; teacher data that is ahead (missing shards, or a global batch size that changes mid-run such as--rampup-batch-size) raises a clear error instead of training on misaligned targets. These checks verify sample positions, not token contents.If the teacher’s saved sequence length is longer than the student’s, the loader trims the teacher tensors to match; the reverse (teacher shorter than student) is not currently handled.
PP > 1 support relies on a per-pipeline-stage collective for remote shard discovery (all last-PP-stage ranks participate) rather than a world-group broadcast, to avoid deadlocking with the loader’s restriction to the last pipeline stage.