Instructions to use jburtoft/Voxtral-Mini-3B-2507-draft-4layer with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use jburtoft/Voxtral-Mini-3B-2507-draft-4layer with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("automatic-speech-recognition", model="jburtoft/Voxtral-Mini-3B-2507-draft-4layer")# Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("jburtoft/Voxtral-Mini-3B-2507-draft-4layer") model = AutoModelForMultimodalLM.from_pretrained("jburtoft/Voxtral-Mini-3B-2507-draft-4layer", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Training scripts
Full reproduction pipeline for training the 4-layer distilled Voxtral draft in this repository. See the main model card for the end-to-end recipe. This document describes each script individually.
All scripts are self-contained and driven by command-line arguments. Defaults assume /mnt/data/ for working data and /mnt/drafts/ for output checkpoints, but every path is overridable via a flag.
Two training paths
The scripts support two Trainium software stacks:
- torch-neuronx XLA (SDK 2.31 DLAMI) -- the default. Uses
torch_xlawithxm.mark_step(). Runs in the standard SDK 2.31 DLAMI'spytorch_2_9venv. Faster steady-state per-step, produced the weights in this repo. - PyTorch Native Beta 4 (SageMaker Training DLC) -- eager mode +
torch.compile(backend='neuron'). Runs in a separate venv created bybeta35-eval'sdeploy-beta35.sh --variant beta4script. Same convergence, slightly slower per-step, simpler port from vanilla PyTorch training code, and the direction AWS is investing in for future Trainium training.
Both paths produce the same trained weights (within run-to-run seed variance). The _beta4 variants of the sanity and training scripts are the Beta 4 ports.
Pipeline diagram
LibriSpeech .flac -----> precompute_pseudo_labels.py ----> {clip}.pt per clip
| (input_ids,
audio_encoder (CPU) + Voxtral greedy --+ audio_embeds,
teacher_token_ids)
|
Trainium training loop: v
for step in range(total_steps): |
batch = load({clip}.pt files, pad to max_seq_len) <----------+
inputs_embeds = splice audio_embeds into text embeds
logits = student(inputs_embeds)
loss = CE(logits, teacher_token_ids) # teacher forcing
backward + step
|
v
/mnt/drafts/best <---- best checkpoint by
val top-1 agreement
on held-out audio
Scripts
| Script | Purpose | Stack |
|---|---|---|
sanity_voxtral_xla.py |
Verify Voxtral loads on trn2 via torch_xla. Expected to fail on the audio path (documents the trn2 sort op limitation). |
XLA |
sanity_voxtral_xla_v2.py |
Verified working XLA path: audio encoder on CPU, LLM decoder on trn2 via torch_xla. |
XLA |
sanity_voxtral_beta4.py |
Beta 4 equivalent of sanity_voxtral_xla_v2.py. Uses torch.device("neuron:0") eager mode + torch.compile(backend='neuron'). Prints wall time for both eager and compiled variants. |
Beta 4 |
build_draft.py |
Take a Voxtral checkpoint and prune to N decoder layers. Saves to --out-dir. Used at training-start to create the initial student. |
Any |
build_librispeech_manifest.py |
Build a CSV manifest from a single LibriSpeech split (dev-clean etc.). |
Any |
build_librispeech_combined_manifest.py |
Build a manifest combining multiple LibriSpeech splits, with de-duplication against a previously-precomputed pseudo-label directory. | Any |
precompute_pseudo_labels.py |
For each audio clip in a manifest: run Voxtral greedy on CPU, save (input_ids, audio_embeds, teacher_token_ids) as <stem>.pt. |
Any (CPU only) |
train_distill.py (v1) |
Simple training loop, no validation. Kept for reference; use v2 instead. | XLA |
train_distill_v2.py (v2) |
Training with periodic validation on a held-out set + best-checkpoint tracking. This is the script that produced this repo's weights. | XLA |
train_distill_v2_beta4.py |
Beta 4 port of v2. Identical arguments + adds --compile flag to wrap the LLM decoder in torch.compile(backend='neuron'). |
Beta 4 |
validate_draft.py |
Standalone quality check for a trained draft. Runs greedy generation with the draft alone, then computes teacher-forced per-position top-1 agreement between draft and target. | Any (CPU) |
Key design choices
Layer selection: [0, 10, 20, 29]
- Includes layer 0 (initial embedding processing) and layer 29 (final projection) for stability.
- Even spacing in between (10, 20) samples different depths of the target's representation.
- 4 layers is ~13% of the target's decoder FLOPs (
4/30). - Not systematically swept. Alternatives like
[0, 7, 15, 22, 29](5 layers) may give a better acceptance/cost tradeoff and are trivial to try by changing--keep-layers.
Pseudo-labels via greedy teacher
- Runs Voxtral end-to-end on each audio clip with greedy decoding.
- Saves target's transcript token IDs; student learns to reproduce them exactly under teacher forcing (cross-entropy loss).
- Alternatives considered:
- KL on soft logits: potentially better quality but requires storing full 131072-vocab distributions per position (~256 MB per 128-token clip, 500+ GB total). Not run.
- Ground-truth transcripts (LibriSpeech gold): risks the student learning phrasings different from Voxtral's, hurting downstream spec-decode acceptance.
- Chose hard-label distillation (teacher's argmax) as the simplest reasonable option.
Frozen audio path
- Audio encoder + projector +
embed_tokens+ finalnorm+lm_headare byte-identical between student and target -- no reason to train them. - Only the 4 decoder layers get gradients (~428M trainable of the ~3B target total).
Batch size 2
- Chose conservatively for a first run. Larger batches (4, 8) are likely feasible and would give more stable gradients. Untested; open for future work.
- Batch size 1 also works but shows higher loss variance.
Cosine LR schedule
- Standard for short distillation runs.
- 100-step linear warmup + cosine decay to 10% of peak LR over remaining steps.
- Peak LR: 1e-4. Reasonable for LLM fine-tuning; not swept.
Trainium-specific workaround (XLA path only)
Root cause -- XLA path only. On the torch-neuronx XLA path, Voxtral's end-to-end forward compile fails with [NCC_EVRF029] Operation sort is not supported on trn2. The op being sorted is s32[1176576] -- a 1D int32 index tensor of size 1 * 383 * 3072 -- which is the flattened form of Voxtral's inputs_embeds [1, 383, 3072]. The sort comes from torch_xla's lowering of masked_scatter in VoxtralModel.forward, specifically the splicing of audio embeddings into inputs_embeds at the audio-token positions. It is not the audio encoder itself. The audio encoder (audio_tower + multi_modal_projector) compiles cleanly on Neuron in both XLA and Beta 4 eager modes.
Verified 2026-07-31:
- XLA
model.model.audio_tower(input_features)-> compiles + returns[1, 1500, 1280]correctly. - XLA
inputs_embeds.masked_scatter(mask, audio_embeds)on[1, 383, 3072]-> fails withsorterror. - Beta 4 eager
inputs_embeds.masked_scatter(...)on the same tensor -> works. - Beta 4 eager
model(**inputs)end-to-end onneuron:0-> works, ~1150 ms per forward.
What the workaround does (used by every script here): precompute audio embeddings on CPU once per training clip, splice them into inputs_embeds also on CPU in the DataLoader collate function, and only send the audio-conditioned inputs_embeds to Neuron. The audio encoder + projector never runs on Neuron in the training loop.
precompute_pseudo_labels.pyrunsmodel.model.get_audio_features(input_features)on CPU. This producesaudio_embedsof shape[375, 3072]per clip.train_distill_v2.py(and its Beta 4 variant) splicesaudio_embedsintoinputs_embedsat the audio-token positions in the collate function (on CPU, per batch).- The Trainium graph only sees
inputs_embeds(already audio-conditioned), notinput_features. No audio encoder involved.
Since the audio path is frozen (never trained), this precomputation only runs once per training clip -- it's cheaper than re-running the frozen encoder every training step regardless of whether it would compile.
Beta 4 note: you could rewrite train_distill_v2_beta4.py to compute audio_embeds on-device inside the training loop and skip the precompute step. It works (see the sanity test), but it's slower per step (1150 ms including audio encoder) than the precompute approach (200-590 ms per step, audio-encoder cost paid once offline). The precompute approach is retained in train_distill_v2_beta4.py for parity with the XLA script and for training efficiency.
Observations from the reference run (XLA path, 3000 steps)
Final training summary (2000 LibriSpeech clips, batch=2):
val step 200: top-1 agreement = 12.82%
val step 400: top-1 agreement = 19.23%
val step 600: top-1 agreement = 25.05%
val step 800: top-1 agreement = 31.36%
val step 1000: top-1 agreement = 36.59%
val step 1200: top-1 agreement = 41.62%
val step 1400: top-1 agreement = 49.01%
val step 1600: top-1 agreement = 51.78%
val step 1800: top-1 agreement = 53.75%
val step 2000: top-1 agreement = 57.40%
val step 2200: top-1 agreement = 62.43%
val step 2400: top-1 agreement = 64.40%
val step 2600: top-1 agreement = 61.93%
val step 2800: top-1 agreement = 66.67% <-- best
val step 3000: top-1 agreement = 65.78%
- Curve was still climbing at step 2400; plateaued at 62-67% for the last 600 steps. Training data appears to be the bottleneck at this scale.
- Data-scaling law observed in earlier runs: 4x more training data (500 -> 2000 clips) produced 1.7x higher final val top-1 (39% -> 67%). Scaling to 10k+ LibriSpeech clips (e.g.,
train-clean-100= ~28k clips) should push val top-1 into the 75-85% range. - Compile bursts happen periodically as new sequence shapes are encountered by the XLA HLO compiler. After the first ~60 s compile the training loop runs at ~130 ms/step steady-state. Save-checkpoint triggers a flush every N steps but is quick.
Beta 4 comparison (500 steps, --compile)
Same hyperparameters, same data, same seed. 500-step run:
| Step | XLA v2 | Beta 4 compiled |
|---|---|---|
| 100 | 8.28% | 6.61% |
| 200 | 9.17% | 9.76% |
| 300 | 10.06% | 10.16% |
| 400 | 14.40% | 15.88% |
| 500 | 18.64% | 18.34% |
Convergence is essentially identical. The two paths produce interchangeable weights.
Per-step wall clock (steady state, batch=2, max_seq_len=512):
| Path | Compile time | Steady-state step |
|---|---|---|
torch-neuronx XLA |
~60 s | ~130 ms |
Beta 4 --compile |
~49 s | ~200-590 ms (some recompiles on val batches) |
Beta 4 eager (no --compile) |
~107 s | ~3700 ms (28x slower) |
XLA is currently the fastest path for this scale. Beta 4 with --compile is 1.5-2x slower total wall time. Beta 4 eager is not competitive.
Recommendation: use train_distill_v2.py (XLA) unless your environment mandates the Beta 4 stack (some SageMaker training clusters, TorchTitan integration, or if you want to try torch.compile fusion on more advanced setups). The Beta 4 script is a drop-in replacement with the same arguments plus --compile.
Known gotchas
- First iteration compile is slow (both paths): 60-150 s per new tensor shape encountered. Subsequent iterations hit the cache. Keep
max_seq_lenconstant across all batches for best cache hit rate. - Checkpoint save is expensive: ~30-40 s per save (writes 3.6 GB safetensors + re-loads the full target for state-dict merging via
load_state_dict(..., strict=False)). Use--save-every 500or larger to minimize interruptions. - CPU pseudo-labeling is slow but embarrassingly parallel: ~5-10 s per clip on a single CPU. 2000 clips takes ~2 hours on one instance. If precompute is the bottleneck, run it on a large-core CPU instance (c7i.16xlarge is much faster than trn2's ~48 CPUs).
- Voxtral's audio path is byte-identical between student and target, so the
.ptfiles produced byprecompute_pseudo_labels.pyare reusable across draft architectures. If you train a 6-layer variant later, the precompute step doesn't need to re-run. - Beta 4-specific: the first log-print says step time is very high (e.g., "step 1 | step time 24768ms"). That's the cumulative-average step time carrying the compile cost. Look at the deltas between successive
elapsedtimestamps to see the true per-step wall time. - Beta 4-specific:
torch.compile(dynamic=False)locks shapes. If your batch shapes vary (e.g., variable-length audio), you'll trigger recompiles and lose the fusion advantage. Pad every batch to a single fixed shape.