Skip to content

Self-distillation + A100 campaign prep - #1

Merged
puneethgv merged 9 commits into
mainfrom
feat/self-distillation
Jun 27, 2026
Merged

Self-distillation + A100 campaign prep#1
puneethgv merged 9 commits into
mainfrom
feat/self-distillation

Conversation

@puneethgv

Copy link
Copy Markdown
Owner

Why

The system is lossless and complete, but the headline — a clearly-visible wall-clock speedup — was gated by drafter acceptance (~1.4, need ≳2.0). Acceptance measures "did the drafter predict the target's argmax?", yet we trained on human-written text (label = human token ≠ target argmax). This PR closes that gap with self-distillation and prepares the A100 campaign to realize the speedup on a big fp16 target (where speedup ≈ acceptance, since the tiny drafter's cost is negligible next to each target forward).

What

  • Self-distillation pipeline (pe.distill): generate the target's own greedy response per prompt, cache features over [prompt | response]. Label = target argmax by construction.
  • Teacher-argmax labels: supervision is taken from the unpadded teacher-forced argmax (same forward as the features), not the generated token — so labels are exact even when batched left-padding flips a near-tie token. Real-model verified: 0 label mismatches.
  • Prompt-region loss masking (mtp_backward(prompt_len=…)): only response-region labels (which equal the target argmax) carry the acceptance signal.
  • Batched generation (--batch-size): ~10× faster data-gen, keeps A100 spot exposure short; single-sequence remains the default.
  • Benchmark the shipped path: run_bench.py now times the cached one-forward loop (generate_speculative_cached vs vanilla_generate_cached); --recompute keeps the old reference.
  • Visible side-by-side demo: cached loop now emits each step's accepted burst in one on_commit, so streaming shows multi-token bursts (verified bursts of 6 at acceptance 5.7, still lossless). make demo (live) + scripts/record_demo.sh (GIF).
  • Ops runbooks: scripts/azure_a100_setup.sh (A100 spot VM + budget guard) and scripts/run_campaign.sh (distill → train → bench → GIF).

Tests

32 tests (was 27), ruff clean. New tests prove: response/batched labels == unpadded target argmax; prompt-masking slot counts; teacher-label path == next-token path when self-consistent.

Not in this PR

The A100 run itself (Qwen2.5-14B fp16) — gated on a free local validation that self-distillation lifts acceptance above the 1.39 baseline.

🤖 Generated with Claude Code

Train the drafter on the target's OWN greedy generations so the next-token
label equals the target's argmax — exactly what decode-time acceptance
measures — directly optimizing acceptance length (the speedup gate).

- pe.distill: generate target greedy response per prompt, featurize
  [prompt | response], shard in FeatureDataset layout + per-example prompt_len
- FeatureDataset(with_prompt_len): yields prompt_len; flags self_distilled caches
- mtp_backward(prompt_len): masks prompt-region labels out of the loss
  (only response labels equal the target argmax) — exact valid-slot count
- train: auto-detects self-distilled caches and masks the prompt region
- tests: response labels == target argmax; masking counts; backward accepts it
- make distill target
Batched greedy generation (HF .generate, left-padded) makes A100 data-gen
~10x faster and avoids long single-sequence spot exposure. Left-padding can
flip occasional near-tie tokens vs unpadded decoding, so supervision is taken
from the unpadded teacher-forced argmax (same forward as the features) rather
than the generated token — labels are exact regardless of generation method.

- distill.distill_batch: left-pad a prompt batch, batched .generate, per-example
  unpadded featurization (positions stay inference-exact)
- _featurize returns per-position labels = target argmax (features+labels from
  one consistent forward); also tightens the single-sequence path
- mtp_backward(teacher_labels): supervise on teacher_labels[tgt-1] when present,
  else next token (human-text caches unchanged)
- FeatureDataset yields labels; train threads them through
- --batch-size CLI flag; single-sequence remains the default
- tests: batched/single labels == unpadded argmax (real-model verified 0 label
  mismatches); teacher-label path == next-token path when self-consistent

31->32 tests, ruff clean.
run_bench timed the prefix-recompute loop (generate_speculative /
vanilla_generate); the headline now uses the KV-cached path we actually ship —
generate_speculative_cached vs vanilla_generate_cached — so reported
speedup/calls-per-token reflect production. --recompute keeps the old loop for
the lossless-equivalence reference. Verified lossless on the toy model for chain
and tree.
The cached one-forward loop emitted on_commit one token at a time, so the
streaming demo could never show multi-token bursts on the path we ship — the
'see the difference' artifact only worked on the slow recompute loop. Emit each
step's accepted burst in a single on_commit call (display only; output_ids
unchanged, still lossless — verified on toy at acceptance 5.7 -> bursts of 6).

- serve.generate_speculative_cached: batch on_commit per step
- scripts/record_demo.sh: asciinema -> agg GIF of naive vs tree
- make demo (live) / make record-demo (GIF), TARGET/CKPT/PROMPT overridable
scripts/azure_a100_setup.sh: provision a single A100 80GB spot VM with a budget
guard (alert at $80 of the $100 credits), NVIDIA driver extension, and the
on-VM bring-up steps; deallocate to stop hourly billing.

scripts/run_campaign.sh: the end-to-end A100 run — batched self-distill ->
prompt-masked train -> cached-loop benchmark -> side-by-side GIF, all
parameterized (TARGET/EXAMPLES/BATCH/LAYERS/DEPTH).
Mid-epoch checkpointing for cloud GPUs where an epoch can outlast the mean
time-between-preemptions. Training saves the model + training position every
save_every updates (atomic write + optional on_save volume-commit hook) and a
restarted process fast-forwards into the in-progress epoch via the deterministic
dataset iterator; the optimizer is re-initialised (Adam's first post-reset step
is self-normalised, far cheaper than persisting ~5GB of moments). A config
signature guards against resuming a mismatched checkpoint. Three new tests cover
resume-continues, finished-run no-op, and mismatched-config-starts-fresh.
infra/modal_campaign.py runs the distill -> train -> bench pipeline on a Modal
A100 (env-overridable target/GPU), with a detach-safe single-container campaign,
a resume-from-cache 'finish' entrypoint (mid-epoch checkpoint + retries for
preemption resilience), and a read-only 'diagnose' entrypoint.

bench/diagnose_acceptance.py measures the drafter's per-depth top-1 accuracy
against the target's own greedy argmax, decomposing acceptance length into the
strong depth-0 prediction vs the data-hungry deeper draft positions.
Scaling the pipeline to an fp16 Qwen2.5-14B target on an A100, the trained 4-layer
drafter crosses break-even: at 15 epochs the parallel-tree loop runs 1.045x vanilla
(target_calls/token 0.82 < 1.0), with acceptance climbing monotonically with
training (1.345 -> 1.447 @6ep -> 1.677 @15ep). README adds the 14B results table,
a per-depth diagnostic explaining the trajectory, and a note framing the remaining
gap to a large speedup as training scale, not architecture. Ignore distill_cache*/.
Adds bench/demo_race.py (record both decoders once, replay each stream, composite
side by side) and bench/sidebyside.py (stitch two terminal GIFs time-aligned). The
Modal gif entrypoint probes several prompts on the saved 14B drafter, keeps the
best-speedup one, and renders the side-by-side demo. README embeds docs/demo.gif and
the 14B benchmark plot, and documents that speedup is much larger on structured/code
prompts (~1.3x, acceptance ~1.9-2.0) than the mixed-prompt average. Also fixes the
streaming demo to decode incrementally so inter-token spaces survive.
@puneethgv
puneethgv merged commit 22ef381 into main Jun 27, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant