Environment
Bug Description
When SequentialPipeline is used with AutoRoundModifier and propagate_error=False
(the AutoRound default), quantize_block begins every block by calling collect_reference
— a full FP16 forward pass over all N/world_size calibration inputs — to obtain reference
outputs for the 200-iter SignSGD loss computation. These outputs are stored in CPU RAM.
The SequentialPipeline already holds the identical tensors in IntermediatesCache[k+1]
(the next subgraph's inputs, which equal the current subgraph's unquantized FP16 outputs).
collect_reference re-derives them from scratch, adding ~39 GB CPU RAM per rank (N=8192,
8-GPU DDP). This duplication exhausts the node's physical memory, causing the OS to
SIGKILL one or more ranks; the remaining ranks then hang on the next NCCL collective
until the 600 s watchdog fires.
Reproduction
Triggers with propagate_error=False (default), large N, and a large model:
recipe = AutoRoundModifier(
targets="Linear",
scheme="W4A16",
iters=200,
# propagate_error defaults to False
)
oneshot(
model="<122B MoE model>",
recipe=recipe,
dataset=calibration_data,
num_calibration_samples=8192, # N=8192 triggers OOM; N=4096 is marginal
)
Observed failure pattern (consistent across all affected runs):
Last successful log line before termination:
[Rank N] apply_autoround | INFO - Applying AutoRound on layer model.layers.X
Then silence — the rank is SIGKILL'd by the OS OOM killer before Python
can write an exception. The DDP launcher reports:
exitcode: -9 (SIGKILL) / exitcode: 1
error_file: <N/A>
Remaining ranks hang on NCCL ALLGATHER until watchdog fires:
[Rank 7] Watchdog caught collective operation timeout:
WorkNCCL(OpType=ALLGATHER) ran for 600425 milliseconds before timing out.
CPU RAM spike observed in Memory Monitor logs right before termination:
After subgraph k calibration completes (IntermediatesCache[k+1] filled):
[Memory Monitor] peak_ram: 310.27 GB ← IntermediatesCache, N=8192, 8 ranks
collect_reference would add another ~310 GB (same tensor, re-derived)
→ total ~620 GB + model weights → exceeds 1.8 TiB node RAM → SIGKILL
Measured Impact
CPU RAM overhead per rank = (N / world_size) × seq × hidden × 2B
| N |
N/rank |
collect_reference CPU RAM overhead/rank |
Without fix |
| 4096 |
512 |
~19 GB (512 × 6144 × 3072 × 2) |
marginal, passes |
| 8192 |
1024 |
~39 GB (1024 × 6144 × 3072 × 2) |
CPU RAM OOM → SIGKILL → NCCL timeout |
With N=8192 and 8 ranks, IntermediatesCache already consumes ~310 GB system RAM.
collect_reference adds another ~310 GB of identical data → total ~620 GB (plus
~244 GB model weights, Python heap, etc.) → exceeds 1.8 TiB node limit.
Root Cause
With propagate_error=False, every subgraph sees the original unquantized activations
as input. Therefore:
IntermediatesCache[k+1] = FP16 output of block k (input = cur_inputs[k], unquantized)
collect_reference output = FP16 output of block k (input = cur_inputs[k], unquantized)
Same block + same input + same unquantized weights → outputs are identical.
CompressionOrchestrator.quantize_block (in auto_round/compressors/orchestrator.py) does
not currently accept reference_output — it always runs the collect_reference forward pass.
llm-compressor's apply_autoround therefore has no way to supply the cached tensors, even
though IntermediatesCache already holds the identical data.
The fix requires adding reference_output=None to both CompressionOrchestrator.quantize_block
and AlgorithmComposer.compress_block (in auto_round/algorithms/composer.py), so the
forward pass can be skipped when a pre-seeded result is provided by the caller.
Proposed Fix
Two coordinated changes — one in llm-compressor (this PR), one companion PR to intel/auto-round:
1. intel/auto-round — add reference_output to quantize_block / compress_block
CompressionOrchestrator.quantize_block and AlgorithmComposer.compress_block need a
new optional parameter. When provided, Step 3 (collect_reference forward pass) is skipped:
auto_round/compressors/orchestrator.py
def quantize_block(self, block, inputs, q_input=None, device="cpu",
auto_offload=True, reference_output=None, ...):
...
new_q_input, reference_output = self.alg_composer.compress_block(
..., reference_output=reference_output,
)
auto_round/algorithms/composer.py
def compress_block(self, block, fp_inputs, input_others, block_ctx,
q_inputs=None, input_ids=None, reference_output=None, **kwargs):
...
quant_hooks = self._get_fp_act_hooks(block)
if reference_output is None: # ← skip forward pass when pre-seeded
reference_output = block_forward_fn(block, fp_inputs, input_others)
...
2. llm-compressor (this PR) — preseed reference_output from IntermediatesCache
-
pipelines/sequential/pipeline.py: Before sequential_epoch_end fires, extract
the next subgraph's hidden states from IntermediatesCache and deliver them via
modifier.set_fp_ref_outputs(hidden_states) (duck-typed, no circular import).
-
modifiers/autoround/base.py: Add _fp_ref_outputs: list | None = PrivateAttr(default=None)
and a set_fp_ref_outputs() method as the public intake. In apply_autoround, consume
_fp_ref_outputs and pass it to quantize_block(reference_output=...), setting the
attribute to None immediately after to release the reference.
When _fp_ref_outputs is present, quantize_block uses it directly and skips
the collect_reference forward pass. When absent (e.g. propagate_error=True, or last subgraph),
behaviour is unchanged — reference_output=None falls back to the existing path.
Prerequisite: PR #3055 (merged) — offloads calibration inputs to CPU, which is a
necessary condition for the memory budget to hold here.
Requires: companion PR to intel/auto-round adding reference_output parameter to
CompressionOrchestrator.quantize_block and AlgorithmComposer.compress_block.
(Parameter does not exist in any released version; must land concurrently.)
Verification
Completion runs
| Run |
Model |
N |
elapsed |
Result |
| Before fix |
Qwen3.5-MoE 122B (Qwen3_5MoeForConditionalGeneration) |
8192 |
— |
CPU RAM OOM, rank SIGKILL'd, NCCL watchdog timeout |
| After fix |
Qwen3.5-MoE 122B (Qwen3_5MoeForConditionalGeneration) |
8192 |
35834s (~10h) |
✅ SUCCESS, 49/49 blocks |
Hardware: 8 × NVIDIA H20 (140 GB VRAM, ~1.8 TiB RAM), 8-GPU DDP, seqlen=6144, iters=200, enable_torch_compile=False.
Memory profile post-fix (N=8192, 8-GPU DDP):
peak system RAM ≈ 310 GB total (~39 GB/rank × 8, IntermediatesCache only, no duplicate)
VRAM allocated ≈ 9.5 GB/rank (one block on GPU during quantize_block, weights offloaded to CPU)
Quantization quality: N scaling (Qwen3.5-MoE 122B, CTK-OFF, seq=6144)
BF16 baseline: GPQA=0.4545, IFEval=0.7449, GSM8K=0.8362, MATH500=0.5260
| Config |
GPQA |
IFEval |
GSM8K |
MATH500 |
GPQA recovery |
| N=4096 (before fix) |
0.4444 |
0.7301 |
0.8393 |
0.5300 |
97.8% |
| N=6144 (before fix) |
0.4444 |
0.7024 ⚠️ |
0.8324 |
0.5200 |
97.8% |
| N=8192 (this fix) |
0.5101 |
0.7338 |
0.8347 |
0.5420 |
112.2% ✅ |
N=4096 and N=6144 were both achievable before the fix; GPQA did not improve with
larger N until the fix enabled N=8192 (+14.6 pp absolute, surpasses BF16 baseline).
N=10240 excluded: uses seq_len=5120 (≠ 6144), not calibration-aligned with the above.
Additional Finding: enable_torch_compile and DDP stability
During validation, a separate failure was observed unrelated to the CPU RAM fix:
AutoRoundModifier defaults to enable_torch_compile=True, which calls torch.compile()
on the block before the optimization loop. In an 8-GPU DDP run with a 122B model,
compilation time varies across ranks (can exceed 10 minutes); ranks that finish earlier
block on an NCCL ALLGATHER waiting for the slow rank, triggering the 600 s watchdog.
Workaround: set enable_torch_compile=False when running multi-GPU DDP with large
models. This is unrelated to the collect_reference fix but is recommended practice:
recipe = AutoRoundModifier(
targets="Linear",
scheme="W4A16",
iters=200,
enable_torch_compile=False, # required for DDP stability on large models
)
This will be noted in the AutoRoundModifier docstring in the accompanying PR.
PR Ready
Environment
main(post-[AutoRound] Offload calibration inputs to CPU to prevent VRAM OOM with large N #3055)Qwen3_5MoeForConditionalGeneration(122B MoE, BF16)Bug Description
When
SequentialPipelineis used withAutoRoundModifierandpropagate_error=False(the AutoRound default),
quantize_blockbegins every block by callingcollect_reference— a full FP16 forward pass over all N/world_size calibration inputs — to obtain reference
outputs for the 200-iter SignSGD loss computation. These outputs are stored in CPU RAM.
The
SequentialPipelinealready holds the identical tensors inIntermediatesCache[k+1](the next subgraph's inputs, which equal the current subgraph's unquantized FP16 outputs).
collect_referencere-derives them from scratch, adding ~39 GB CPU RAM per rank (N=8192,8-GPU DDP). This duplication exhausts the node's physical memory, causing the OS to
SIGKILL one or more ranks; the remaining ranks then hang on the next NCCL collective
until the 600 s watchdog fires.
Reproduction
Triggers with
propagate_error=False(default), large N, and a large model:Observed failure pattern (consistent across all affected runs):
CPU RAM spike observed in Memory Monitor logs right before termination:
Measured Impact
With N=8192 and 8 ranks,
IntermediatesCachealready consumes ~310 GB system RAM.collect_referenceadds another ~310 GB of identical data → total ~620 GB (plus~244 GB model weights, Python heap, etc.) → exceeds 1.8 TiB node limit.
Root Cause
With
propagate_error=False, every subgraph sees the original unquantized activationsas input. Therefore:
Same block + same input + same unquantized weights → outputs are identical.
CompressionOrchestrator.quantize_block(inauto_round/compressors/orchestrator.py) doesnot currently accept
reference_output— it always runs the collect_reference forward pass.llm-compressor's
apply_autoroundtherefore has no way to supply the cached tensors, eventhough
IntermediatesCachealready holds the identical data.The fix requires adding
reference_output=Noneto bothCompressionOrchestrator.quantize_blockand
AlgorithmComposer.compress_block(inauto_round/algorithms/composer.py), so theforward pass can be skipped when a pre-seeded result is provided by the caller.
Proposed Fix
Two coordinated changes — one in llm-compressor (this PR), one companion PR to intel/auto-round:
1. intel/auto-round — add
reference_outputtoquantize_block/compress_blockCompressionOrchestrator.quantize_blockandAlgorithmComposer.compress_blockneed anew optional parameter. When provided, Step 3 (
collect_referenceforward pass) is skipped:2. llm-compressor (this PR) — preseed
reference_outputfromIntermediatesCachepipelines/sequential/pipeline.py: Beforesequential_epoch_endfires, extractthe next subgraph's hidden states from
IntermediatesCacheand deliver them viamodifier.set_fp_ref_outputs(hidden_states)(duck-typed, no circular import).modifiers/autoround/base.py: Add_fp_ref_outputs: list | None = PrivateAttr(default=None)and a
set_fp_ref_outputs()method as the public intake. Inapply_autoround, consume_fp_ref_outputsand pass it toquantize_block(reference_output=...), setting theattribute to
Noneimmediately after to release the reference.When
_fp_ref_outputsis present,quantize_blockuses it directly and skipsthe collect_reference forward pass. When absent (e.g.
propagate_error=True, or last subgraph),behaviour is unchanged —
reference_output=Nonefalls back to the existing path.Prerequisite: PR #3055 (merged) — offloads calibration inputs to CPU, which is a
necessary condition for the memory budget to hold here.
Requires: companion PR to intel/auto-round adding
reference_outputparameter toCompressionOrchestrator.quantize_blockandAlgorithmComposer.compress_block.(Parameter does not exist in any released version; must land concurrently.)
Verification
Completion runs
Qwen3_5MoeForConditionalGeneration)Qwen3_5MoeForConditionalGeneration)Hardware: 8 × NVIDIA H20 (140 GB VRAM, ~1.8 TiB RAM), 8-GPU DDP, seqlen=6144, iters=200,
enable_torch_compile=False.Memory profile post-fix (N=8192, 8-GPU DDP):
Quantization quality: N scaling (Qwen3.5-MoE 122B, CTK-OFF, seq=6144)
BF16 baseline: GPQA=0.4545, IFEval=0.7449, GSM8K=0.8362, MATH500=0.5260
N=4096 and N=6144 were both achievable before the fix; GPQA did not improve with
larger N until the fix enabled N=8192 (+14.6 pp absolute, surpasses BF16 baseline).
N=10240 excluded: uses seq_len=5120 (≠ 6144), not calibration-aligned with the above.
Additional Finding:
enable_torch_compileand DDP stabilityDuring validation, a separate failure was observed unrelated to the CPU RAM fix:
AutoRoundModifierdefaults toenable_torch_compile=True, which callstorch.compile()on the block before the optimization loop. In an 8-GPU DDP run with a 122B model,
compilation time varies across ranks (can exceed 10 minutes); ranks that finish earlier
block on an NCCL ALLGATHER waiting for the slow rank, triggering the 600 s watchdog.
Workaround: set
enable_torch_compile=Falsewhen running multi-GPU DDP with largemodels. This is unrelated to the
collect_referencefix but is recommended practice:This will be noted in the
AutoRoundModifierdocstring in the accompanying PR.PR Ready