Block-wise quantization-aware distillation
QADModifier refines quantized weights by reconstructing decoder blocks'
unquantized outputs. Put it after the quantization method in the same recipe.
QAD shares that method's calibration data and uses the existing sequential
pipeline. It does not load a separate teacher model.
sequential_targets_per_subgraph sets how many consecutive decoder blocks QAD
trains jointly. Each block takes the previous block's fake-quantized output, and
the loss compares the last block's output with its unquantized output. Training
memory grows with the block count, since backpropagation runs through every
block in the subgraph.
from llmcompressor import oneshot
from llmcompressor.modifiers.gptq import GPTQModifier
from llmcompressor.modifiers.qad import QADModifier
from llmcompressor.modifiers.quantization import QuantizationModifier
# Choose a preceding weight quantization method.
quantizer = QuantizationModifier(scheme="NVFP4A16", ignore=["lm_head"]) # RTN
# quantizer = GPTQModifier(scheme="NVFP4A16", ignore=["lm_head"], actorder="static")
oneshot(
model=model,
dataset=dataset,
recipe=[
quantizer,
QADModifier(
num_epochs=3,
lr=2e-6,
gradient_accumulation_steps=4,
),
],
pipeline="sequential",
sequential_targets=["LlamaDecoderLayer"],
# Increase this to train consecutive decoder blocks jointly.
sequential_targets_per_subgraph=1,
propagate_error=True, # default; recommended for QAD
num_calibration_samples=512,
max_seq_length=2048,
batch_size=1,
)
See llama3_example.py for a complete NVFP4A16 example with BF16 model execution and activations:
python llama3_example.py --quantizer rtn --output ./llama-rtn-qad
python llama3_example.py --quantizer gptq --output ./llama-gptq-qad
python llama3_example.py --quantizer gptq --targets-per-subgraph 2 --output ./llama-gptq-qad-2
The example defaults to one UltraChat dataset for calibration and QAD. Override it with
--dataset, --split, and --text-column; --samples and --max-seq-length
apply to both stages. Chat datasets use the model's chat template.
ModelOpt data mix
Use --dataset modelopt_qad_mix to load the seven-source blend from
NVIDIA Model Optimizer's QAT/QAD example.
The loader lives in modelopt_mix.py; both the preceding
quantizer and QAD receive the resulting dataset.
| Hugging Face dataset | Split | Relative weight | Samples when requesting 1024 |
|---|---|---|---|
nvidia/Nemotron-SWE-v1 |
r2e_gym |
6000 | 323 |
nvidia/Nemotron-Math-v2 |
medium |
2500 | 135 |
nvidia/Nemotron-Science-v1 |
MCQ |
1500 | 81 |
nvidia/Nemotron-Science-v1 |
RQA |
1500 | 81 |
nvidia/Nemotron-Instruction-Following-Chat-v1 |
chat_if |
5000 | 269 |
nvidia/Nemotron-Post-Training-Dataset-v2 |
chat |
1500 | 81 |
nvidia/Nemotron-Competitive-Programming-v1 |
competitive_coding_python_part00 |
1000 | 54 |
Run from this directory, selecting the sample count and epoch budget:
python llama3_example.py --dataset modelopt_qad_mix --quantizer gptq --samples 512 --epochs 3 --max-seq-length 2048 --output ./llama-mix-512-e3
python llama3_example.py --dataset modelopt_qad_mix --quantizer gptq --samples 1024 --epochs 7 --max-seq-length 2048 --output ./llama-mix-1024-e7
Weights specify proportions of conversations, not tokens. The loader rounds
allocations to exactly --samples, streams pinned dataset revisions with seed
42 and a bounded shuffle buffer, applies the model's chat template, and truncates
to --max-seq-length. It records source revisions and counts in
dataset.info.description. --split and --text-column apply only to a single
Hugging Face dataset, not this preset. Access to
nvidia/Nemotron-Post-Training-Dataset-v2 requires an authorized Hugging Face
token; loading fails if any required source is inaccessible or too short.
This example adopts the upstream sources and mixture weights. QAD uses its own
validation split described below; --epochs is the per-subgraph epoch count, with
the best validation weights restored before propagation. These
commands demonstrate loading and training, not the full configuration used for
the long-context epoch-ablation benchmark results.
Execution
For each sequential subgraph of one or more consecutive decoder blocks:
- During calibration, hooks cache the blocks' inputs and the last block's unquantized outputs. The preceding method collects its statistics in the same forward pass.
- At the existing
sequential_epoch_endevent, preceding modifiers finish preparing the blocks' quantized weights and quantization parameters. - QAD replays the blocks as a chain with fake quantization, feeding each block the previous block's output, and minimizes the last block's output MSE. Only weights configured for quantization are trained. Calibration and capture hooks are disabled throughout QAD training and validation. Weight observers update qparams before training and after each epoch.
- QAD restores the weights with the lowest validation loss, which may be the untrained weights, re-observes their qparams, materializes quantized weights, and releases the training cache. The pipeline propagates the final outputs to the next subgraph.
The target is local: the original current blocks receive the same cached
input as the student. With subgraph function f (the composition of its
blocks), original weights W0, student weights W, and cached input h, QAD
fits f(h, Q(W)) to f(h, W0). Inside the student chain, later blocks take the
earlier blocks' quantized outputs. This captures the current blocks before the
preceding quantizer changes them; it does not reconstruct an independent
original-model activation stream.
propagate_error selects h. With the default propagate_error=True
(recommended), h is the preceding subgraphs' quantized output, so each subgraph
trains on the inputs it receives at inference. With propagate_error=False,
h is the original model's activation, which gives plain block-wise
reconstruction.
Replay uses each complete target module's forward, retaining its internal
attention, MLP, branches, and residual connections. A subgraph can contain one
or more target modules, and multiple modules train jointly as a single chain:
each module after the first must take the previous module's output as its first
input, as decoder layers do. Other connections between target modules are not
supported.
Composing quantization methods
QAD does not depend on GPTQ Hessians or quantizer class names. Tiny Llama
integration tests cover RTN (QuantizationModifier), GPTQ (GPTQModifier), and
AWQ followed by RTN, each followed by QAD, with NVFP4A16 and integer W4A16.
Another method can precede QAD if it:
- Attaches a compressed-tensors weight quantization scheme during initialization.
- Preserves the unquantized block computation during the calibration pass used to capture teacher targets.
- Prepares its weights and valid qparams in
sequential_epoch_end, before QAD's callback runs. Place QAD after all modifiers that prepare the block. - Leaves floating-point weights compatible with compressed-tensors fake quantization, and suppresses calibration hooks during its own replay forwards.
A transform such as AWQ or SmoothQuant must be composed with a weight quantization modifier. For example, the tested AWQ recipe is:
from llmcompressor.modifiers.transform.awq import AWQModifier
recipe = [
AWQModifier(n_grid=4),
QuantizationModifier(scheme="NVFP4A16", ignore=["lm_head"]),
QADModifier(),
]
Pass this recipe to oneshot with the sequential settings above. The AWQ
integration tests check per-block optimization, cache cleanup, and finite model
outputs; save/reload/generation tests separately cover RTN and GPTQ. Other
transform/quantizer combinations need their own validation. Methods that change
target boundaries or overwrite teacher weights before capture do not satisfy the
contract automatically.
Training and supported scope
- Use single-process sequential calibration. A subgraph can contain one or more
complete target modules; when it contains multiple targets, they must form the
chain described in Execution.
propagate_error=True(the default) is recommended. Quantized weights outside the selected targets, such as a quantized LM head, keep the preceding modifier's result. Repeated calls to the same target in a calibration batch are unsupported. - The same samples, preprocessing, sequence lengths, and loader batch size feed
both calibration and QAD. There is no
qad_datasetorteacher_modeoption.gradient_accumulation_stepscontrols the QAD update batch size. - At least two calibration batches are required. A deterministic split with
validation_fraction=0.1holds batches out from QAD gradient updates, although they still participate in quantizer calibration. Keep downstream test data separate. - Each subgraph uses one optimizer to train its targets jointly for
num_epochsepochs. The weights with the lowest validation loss are then restored; the quantizer's initial weights are included among the candidates. - FP16/BF16 execution uses FP32 optimizer master weights. FP16 backward also
uses dynamic loss scaling to retain small reconstruction gradients. Overflow
retries use the same accumulation group with a lower scale, up to 32 attempts.
Gradients are unscaled before clipping with
max_grad_norm=1.0by default; set it toNoneto disable clipping. - Inputs, teacher outputs, and best-weight snapshots default to CPU storage
through
offload_device="cpu". QAD still needs memory for the subgraph's backward pass and optimizer state. - QAD stores independent input and teacher snapshots in the shared
IntermediatesCacheand onloads one batch at a time during training and evaluation, while preserving the training shuffle and validation split. - QAD computes reconstruction MSE over all output positions.
- Weight re-observation follows Charles's schedule: before training,
after every epoch, and before final materialization. Set
reobserve_weights=Falsefor fixed weight qparams. Scales are not optimized by gradient descent. QAD does not re-observe activations; current validation covers weight-only quantization. Activation quantization needs separate gradient and calibration validation. - Re-observation requires the preceding method to retain live weight observers. All modules are observed before any qparams are updated, preserving shared global scales in fused NVFP4 projections. Best-checkpoint restoration restores weights only; final re-observation then recomputes qparams from them and can change validation MSE again, so the final loss is logged separately from the best loss. Per-target updates, validation losses, and re-observation stages are reported through the log.
Save the resulting model through the normal compressed checkpoint path.