Verify numerical parity between NeMo AutoModel implementations and reference HuggingFace models, including state dict and forward-pass checks.
when_to_use
Verifying numerical correctness of a new or modified model against its HuggingFace reference, debugging loss divergence or output mismatches, or validating state dict mappings.
Parity Testing Skill for NeMo AutoModel
NeMo AutoModel adds custom model implementations (combined projections, backend switching, kernel patches) on top of HuggingFace transformers. Parity testing verifies that NeMo AutoModel's implementation produces numerically equivalent results to the reference HF implementation.
Key differences that can cause divergence:
Combined QKV projections (interleaved layout) vs separate Q/K/V
Combined GateUp MLP vs separate gate/up projections
TE attention vs SDPA vs flex attention backends
TE linear vs torch linear
FP8/BF16 precision differences
RoPE implementation differences
State dict adapter conversion (from_hf/to_hf round-trip)
Kernel patches (Liger kernels, etc.)
Setup
Identify the two implementations
from transformers import AutoModelForCausalLM
from nemo.collections.llm import NeMoAutoModelForCausalLM
The HF model is the reference. The NeMo AutoModel is the implementation under test.
# NeMo way
nemo_model = NeMoAutoModelForCausalLM.from_pretrained()
hf_model = AutoModelForCausalLM.from_pretrained()
"meta-llama/Llama-3.2-1B"
# HF way
"meta-llama/Llama-3.2-1B"
Create identical inputs
Use seeded random tensors to guarantee reproducibility across runs.
GPU introduces non-determinism from parallel reductions and kernel launch order. Always start parity testing on CPU with float32 to isolate numerical differences caused by model implementation from those caused by hardware.
Strict tolerance: max_diff < 1e-5 for float32 on CPU. This is tight enough to catch weight loading bugs while allowing for minor floating-point operation reordering.
Tolerances for bfloat16: max_diff < 1e-2, cosine_similarity > 0.9999. bfloat16 has limited mantissa bits, so per-element differences accumulate across layers.
Comparison Utilities
defcompare_tensors(a, b, name=""):
"""Compare two tensors and report multiple similarity metrics.
Args:
a: Reference tensor (from HF model).
b: Test tensor (from NeMo AutoModel).
name: Label for the comparison (printed in output).
Returns:
Tuple of (max_diff, mean_diff, cosine_similarity).
"""
max_diff = (a - b).abs().max().item()
mean_diff = (a - b).abs().mean().item()
cos_sim = torch.nn.functional.cosine_similarity(
a.flatten().float(), b.flatten().float(), dim=0
).item()
print(
f"{name}: max_diff={max_diff:.6e}, mean_diff={mean_diff:.6e}, "f"cosine_sim={cos_sim:.8f}"
)
return max_diff, mean_diff, cos_sim
defcompare_state_dicts(sd_a, sd_b, prefix=""):
"""Compare two state dicts key-by-key, reporting per-parameter differences."""
keys_a = set(sd_a.keys())
keys_b = set(sd_b.keys())
missing = keys_a - keys_b
extra = keys_b - keys_a
if missing:
print(f"{prefix}Missing keys: {missing}")
if extra:
print(f"{prefix}Extra keys: {extra}")
shared = keys_a & keys_b
max_diffs = {}
for key insorted(shared):
diff = (sd_a[key].float() - sd_b[key].float()).abs().max().item()
if diff > 0:
max_diffs[key] = diff
print(f"{prefix}{key}: max_diff={diff:.6e}")
ifnot max_diffs andnot missing andnot extra:
print(f"{prefix}All {len(shared)} parameters match exactly.")
return missing, extra, max_diffs
Debugging Workflow
Follow this procedure when a parity test fails.
Step 1: If E2E fails, isolate to component level
Run Level 2 component tests. Determine which component (attention, MLP, norm, RoPE, decoder layer) introduces the divergence.
Step 2: If component fails, check weight loading
Verify the state dict adapter round-trip (Level 1). If round-trip is not exact, the bug is in the adapter's from_hf() or to_hf() method.
# Quick check: load NeMo model, export its weights back to HF format, compare
nemo_sd = nemo_model.state_dict()
exported_hf_sd = adapter.to_hf(nemo_sd)
compare_state_dicts(hf_sd, exported_hf_sd, prefix="weight_check: ")
Step 3: If weights match but output differs, check backend
Different backends (TE vs SDPA vs flex attention) can produce different results even with identical weights. Force the baseline backend for parity testing:
from nemo.collections.llm import BackendConfig
# Force SDPA attention and torch linear to match HF behavior
nemo_model = NeMoAutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-3.2-1B",
backend_config=BackendConfig(attn="sdpa", linear="torch"),
)
Step 4: Injection technique
Replace one NeMo component's output with the HF component's output and check if downstream computation matches. This isolates exactly which component introduces divergence.
# Example: inject HF attention output into NeMo decoder layerwith torch.no_grad():
# Get HF attention output
hf_attn_out = hf_model.model.layers[0].self_attn(hidden_states, position_ids=position_ids)
# Manually run NeMo decoder layer but substitute HF attention output
nemo_layer = nemo_model.model.layers[0]
residual = hidden_states
normed = nemo_layer.input_layernorm(hidden_states)
# Use HF attention output instead of NeMo attention output
attn_out = hf_attn_out[0]
hidden_states_after_attn = residual + attn_out
# Continue with NeMo MLP
residual = hidden_states_after_attn
normed = nemo_layer.post_attention_layernorm(hidden_states_after_attn)
mlp_out = nemo_layer.mlp(normed)
final = residual + mlp_out
# If final matches HF decoder layer output, the bug is in NeMo attention.# If final does NOT match, the bug is in NeMo MLP or norm.
Step 5: Gradient parity
After forward pass parity is confirmed, verify gradients:
Always test on CPU/float32 first. GPU and lower precision introduce noise that masks real bugs.
Test both fresh load and save/reload cycle. A model that works after from_pretrained may break after save_pretrained + from_pretrained if the state dict adapter has asymmetries.
Never modify reference HF code. The HF model is the ground truth. Only modify the NeMo AutoModel implementation.
Use deterministic inputs (torch.manual_seed). Every test must be reproducible.
Compare all outputs, not just loss. Loss can match by coincidence even when logits diverge. Always compare logits, hidden states, and attention weights where possible.
Check both forward pass and gradient computation. Forward parity does not guarantee backward parity, especially with custom kernels.
Verify tied weights are handled correctly. If tie_word_embeddings=True, confirm that lm_head.weight and embed_tokens.weight share the same tensor after loading.
Test with and without kernel patches. Liger kernels, SDPA patching, and other optimizations may change numerics. Run parity tests with all patches disabled first, then enable them one at a time.
Code Anchors
These are the key source files relevant to parity testing: