Skip to content

Commit e7f4d86

Browse files
pjordanandrsnclaude
andcommitted
test: pin compile / checkpointing / AMP / meta-device composition
The four probes a reviewer aims at a custom autograd Function, pinned as assertions rather than prose: - torch.compile: Dynamo graph-breaks cleanly on the data-dependent expert routing (aten.nonzero) and the Function runs eagerly inside the compiled wrapper — forward and input-grad stay bitwise-equal to eager. - gradient checkpointing: composes with recompute-in-backward at 3x dequants per fwd+bwd vs 2x uncheckpointed (+50%, not a multiply), bit-exact either way; the count is asserted, not documented. - autocast: the dequant path is untouched (state bit-identical, weights still materialize in compute_dtype), output dtype does not drift, values match the non-autocast forward at bf16 precision. - meta-device: init-under-torch.device('meta') + load_state_dict(assign= True) — the HF init_empty_weights-style path — materializes packed weights and absmax with no meta leftovers, frozen flag intact, forward bit-identical to the source module. Works with zero custom hooks because the codebook is rebuilt at init rather than serialized. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 061b091 commit e7f4d86

1 file changed

Lines changed: 143 additions & 0 deletions

File tree

tests/test_experts4bit.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,3 +575,146 @@ def test_experts4bit_cuda_dequant_fidelity():
575575
ref = _reference_forward(gate_up, down, hidden_states, top_k_index, top_k_weights)
576576
rel_err = ((out - ref).norm() / ref.norm()).item()
577577
assert rel_err <= 0.2, f"4-bit forward rel err {rel_err:.3f} above ceiling"
578+
579+
580+
# ---------------------------------------------------------------------------
581+
# Composition: torch.compile, gradient checkpointing, autocast, meta-device
582+
# ---------------------------------------------------------------------------
583+
584+
585+
@pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
586+
def test_experts4bit_torch_compile_parity_and_breaks():
587+
"""torch.compile falls back cleanly on the routing and never changes the math.
588+
589+
The forward's expert routing is data-dependent (`nonzero` on the expert mask), so
590+
Dynamo graph-breaks there and `_FrozenLinearRecomputeBackward` runs eagerly inside
591+
the compiled wrapper (observed: 5 breaks / 6 graphs on torch 2.6, all attributed to
592+
`aten.nonzero`). That split is acceptable and pinned; what must never happen is a
593+
silently different number — forward and input-grad are asserted bitwise-equal to
594+
eager.
595+
"""
596+
torch._dynamo.reset()
597+
gate_up, down = _random_expert_weights(torch.float32, "cuda")
598+
module = Experts4bit.from_float(gate_up, down, compute_dtype=torch.float32)
599+
hidden_states, top_k_index, top_k_weights = _random_routing("cuda")
600+
601+
x_eager = hidden_states.clone().requires_grad_(True)
602+
out_eager = module(x_eager, top_k_index, top_k_weights)
603+
out_eager.sum().backward()
604+
605+
explanation = torch._dynamo.explain(module)(hidden_states, top_k_index, top_k_weights)
606+
assert explanation.graph_break_count >= 1 # clean break on the routing, not a silent trace
607+
608+
torch._dynamo.reset()
609+
x_compiled = hidden_states.clone().requires_grad_(True)
610+
compiled = torch.compile(module)
611+
out_compiled = compiled(x_compiled, top_k_index, top_k_weights)
612+
out_compiled.sum().backward()
613+
614+
torch.testing.assert_close(out_compiled, out_eager, rtol=0, atol=0)
615+
torch.testing.assert_close(x_compiled.grad, x_eager.grad, rtol=0, atol=0)
616+
torch._dynamo.reset()
617+
618+
619+
@pytest.mark.parametrize("device", get_available_devices())
620+
def test_experts4bit_gradient_checkpoint_recompute_count(device):
621+
"""Checkpointing composes with recompute-in-backward: 3x dequants, not 4x, bit-exact.
622+
623+
Per fwd+bwd, the module alone dequantizes 2*D times (D in forward, D re-dequantized in
624+
backward). Under `torch.utils.checkpoint` the count is 3*D — the no-grad forward, the
625+
checkpoint replay, and the backward re-dequant — i.e. recompute-inside-recompute adds
626+
+50%, it does not multiply. Numerics are bitwise-identical either way.
627+
"""
628+
from torch.utils.checkpoint import checkpoint
629+
630+
gate_up, down = _random_expert_weights(torch.float32, device)
631+
module = Experts4bit.from_float(gate_up, down, compute_dtype=torch.float32)
632+
hidden_states, top_k_index, top_k_weights = _random_routing(device)
633+
634+
counter = {"n": 0}
635+
inner = module._dequantize_expert
636+
637+
def counting(*args, **kwargs):
638+
counter["n"] += 1
639+
return inner(*args, **kwargs)
640+
641+
module._dequantize_expert = counting
642+
643+
with torch.no_grad():
644+
module(hidden_states, top_k_index, top_k_weights)
645+
dequants_per_forward = counter["n"]
646+
assert dequants_per_forward > 0
647+
648+
counter["n"] = 0
649+
x_plain = hidden_states.clone().requires_grad_(True)
650+
out_plain = module(x_plain, top_k_index, top_k_weights)
651+
out_plain.sum().backward()
652+
assert counter["n"] == 2 * dequants_per_forward
653+
654+
counter["n"] = 0
655+
x_ckpt = hidden_states.clone().requires_grad_(True)
656+
out_ckpt = checkpoint(module, x_ckpt, top_k_index, top_k_weights, use_reentrant=False)
657+
out_ckpt.sum().backward()
658+
assert counter["n"] == 3 * dequants_per_forward
659+
660+
torch.testing.assert_close(out_ckpt, out_plain, rtol=0, atol=0)
661+
torch.testing.assert_close(x_ckpt.grad, x_plain.grad, rtol=0, atol=0)
662+
663+
664+
@pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
665+
def test_experts4bit_autocast_semantics():
666+
"""Under `torch.autocast` the quantization path is untouched and dtypes don't drift.
667+
668+
The dequantize step is not an autocast op, so weights still materialize in
669+
compute_dtype and the packed/absmax/code state is bit-identical after an autocast
670+
forward; only the linears run in the autocast dtype. The output dtype follows
671+
compute_dtype (fp32 here), and values match the non-autocast forward at bf16
672+
precision (measured max-abs diff ~0.011 at this scale on an RTX A2000).
673+
"""
674+
gate_up, down = _random_expert_weights(torch.float32, "cuda")
675+
module = Experts4bit.from_float(gate_up, down, compute_dtype=torch.float32)
676+
hidden_states, top_k_index, top_k_weights = _random_routing("cuda")
677+
678+
deq_before = module._dequantize_expert(
679+
module.gate_up_proj, module.gate_up_absmax, module._gate_up_shape, 0, torch.float32
680+
)
681+
out_ref = module(hidden_states, top_k_index, top_k_weights)
682+
with torch.autocast("cuda", dtype=torch.bfloat16):
683+
out_amp = module(hidden_states, top_k_index, top_k_weights)
684+
deq_after = module._dequantize_expert(
685+
module.gate_up_proj, module.gate_up_absmax, module._gate_up_shape, 0, torch.float32
686+
)
687+
688+
assert out_amp.dtype == out_ref.dtype == torch.float32
689+
assert module.gate_up_absmax.dtype == torch.float32 and module.code.dtype == torch.float32
690+
torch.testing.assert_close(deq_after, deq_before, rtol=0, atol=0)
691+
torch.testing.assert_close(out_amp, out_ref, rtol=0.05, atol=0.03)
692+
693+
694+
@pytest.mark.parametrize("device", get_available_devices())
695+
def test_experts4bit_meta_device_assign_materialization(device):
696+
"""The `init_empty_weights`-style loading path works with no custom hooks.
697+
698+
Construct under `torch.device("meta")` (what HF `from_pretrained(device_map=...)`
699+
does around module init), then materialize with `load_state_dict(..., assign=True)`.
700+
Packed weights and absmax buffers land as real tensors, the frozen flag survives,
701+
no meta tensors remain (`code` is rebuilt at init and never serialized), and the
702+
forward is bit-identical to the source module.
703+
"""
704+
gate_up, down = _random_expert_weights(torch.float16, device)
705+
src = Experts4bit.from_float(gate_up, down, compute_dtype=torch.float16)
706+
707+
with torch.device("meta"):
708+
empty = Experts4bit(NUM_EXPERTS, HIDDEN_DIM, INTERMEDIATE_DIM, compute_dtype=torch.float16)
709+
assert empty.gate_up_proj.is_meta # ctor really did defer allocation
710+
711+
empty.load_state_dict(src.state_dict(), assign=True)
712+
713+
leftovers = [n for n, t in list(empty.named_parameters()) + list(empty.named_buffers()) if t.is_meta]
714+
assert leftovers == []
715+
assert not empty.gate_up_proj.requires_grad
716+
717+
hidden_states, top_k_index, top_k_weights = _random_routing(device)
718+
out_src = src(hidden_states, top_k_index, top_k_weights)
719+
out_loaded = empty(hidden_states, top_k_index, top_k_weights)
720+
torch.testing.assert_close(out_loaded, out_src, rtol=0, atol=0)

0 commit comments

Comments
 (0)