mamba-cot-tts / code /tests /test_loader_guard.py
maxpicy's picture
Add files using upload-large-folder tool
6e802a4 verified
Raw History Blame Contribute Delete
10.3 kB
# test_loader_guard.py: The checkpoint loader refuses silent drops (LoRA, plan tensors, speaker table).
import sys
from pathlib import Path
import pytest
import torch
import torch.nn as nn
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from train import (LoRALinear, _remap_plain_keys_for_lora, checkpoint_speaker_rows,
load_checkpoint)
class TinyPlain(nn.Module):
def __init__(self):
super().__init__()
self.in_proj = nn.Linear(4, 8, bias=False)
self.out_proj = nn.Linear(8, 4, bias=False)
self.embedding = nn.Embedding(10, 4)
class TinyLora(nn.Module):
def __init__(self, rank=2):
super().__init__()
self.in_proj = LoRALinear(nn.Linear(4, 8, bias=False), rank=rank,
alpha=rank, dropout=0.0)
self.out_proj = LoRALinear(nn.Linear(8, 4, bias=False), rank=rank,
alpha=rank, dropout=0.0)
self.embedding = nn.Embedding(10, 4)
def _save_ckpt(tmp_path, sd, name="ckpt.pt", step=123):
path = tmp_path / name
torch.save({"step": step, "model_state_dict": sd}, path)
return str(path)
def test_remap_plain_projection_keys():
plain_sd = TinyPlain().state_dict()
lora_sd = TinyLora().state_dict()
remapped, n = _remap_plain_keys_for_lora(plain_sd, lora_sd)
assert n == 2, f"expected both projections remapped, got {n}"
assert "in_proj.original.weight" in remapped
assert "out_proj.original.weight" in remapped
assert "in_proj.weight" not in remapped
assert "embedding.weight" in remapped
torch.testing.assert_close(remapped["in_proj.original.weight"],
plain_sd["in_proj.weight"])
def test_remap_leaves_matching_and_unknown_keys_alone():
lora_model_sd = TinyLora().state_dict()
sd = {
"embedding.weight": torch.zeros(10, 4),
"totally.unknown.weight": torch.zeros(2, 2),
}
remapped, n = _remap_plain_keys_for_lora(sd, lora_model_sd)
assert n == 0
assert set(remapped) == set(sd)
def test_fullft_ckpt_loads_exactly_into_lora_model(tmp_path):
src = TinyPlain()
with torch.no_grad():
src.in_proj.weight += 1.0
path = _save_ckpt(tmp_path, src.state_dict())
dst = TinyLora()
step = load_checkpoint(path, dst, device="cpu")
assert step == 123
torch.testing.assert_close(dst.in_proj.original.weight, src.in_proj.weight)
x = torch.randn(3, 4)
torch.testing.assert_close(dst.in_proj(x), src.in_proj(x))
def test_lora_ckpt_into_plain_model_still_errors(tmp_path):
path = _save_ckpt(tmp_path, TinyLora().state_dict())
with pytest.raises(RuntimeError, match="LoRA adapters"):
load_checkpoint(path, TinyPlain(), device="cpu")
def test_dropped_projection_weight_is_a_hard_error(tmp_path):
sd = TinyPlain().state_dict()
sd["mixer.in_proj.weight"] = torch.zeros(8, 4)
path = _save_ckpt(tmp_path, sd)
with pytest.raises(RuntimeError, match="silently dropped"):
load_checkpoint(path, TinyPlain(), device="cpu")
class TinyNeoX(nn.Module):
def __init__(self):
super().__init__()
self.attention = nn.Module()
self.attention.query_key_value = nn.Linear(4, 12, bias=False)
self.attention.dense = nn.Linear(4, 4, bias=False)
self.embedding = nn.Embedding(10, 4)
def test_neox_projection_unexpected_is_a_hard_error(tmp_path):
sd = TinyNeoX().state_dict()
sd["layers.0.attention.query_key_value.weight"] = torch.zeros(12, 4)
path = _save_ckpt(tmp_path, sd)
with pytest.raises(RuntimeError, match="silently dropped"):
load_checkpoint(path, TinyNeoX(), device="cpu")
def test_missing_projection_is_a_hard_error(tmp_path):
sd = TinyNeoX().state_dict()
del sd["attention.query_key_value.weight"]
path = _save_ckpt(tmp_path, sd)
with pytest.raises(RuntimeError, match="received NO value"):
load_checkpoint(path, TinyNeoX(), device="cpu")
def test_hybrid_graft_missing_attention_is_allowed(tmp_path):
class TinyHybrid(nn.Module):
def __init__(self):
super().__init__()
self.backbone = nn.Module()
self.backbone.backbone = nn.Module()
self.backbone.backbone.layers = nn.ModuleDict({
"0": nn.ModuleDict({"mixer": nn.ModuleDict(
{"in_proj": nn.Linear(4, 12, bias=False)})}),
})
self.hybrid_replaced_layers = [0]
model = TinyHybrid()
sd = {k: v for k, v in model.state_dict().items()
if not k.endswith("in_proj.weight")}
path = _save_ckpt(tmp_path, sd)
load_checkpoint(path, model, device="cpu")
def test_architecture_mismatched_projection_shape_is_a_hard_error(tmp_path):
sd = TinyPlain().state_dict()
sd["in_proj.weight"] = torch.zeros(24, 4)
path = _save_ckpt(tmp_path, sd)
with pytest.raises(RuntimeError, match="different SHAPE"):
load_checkpoint(path, TinyPlain(), device="cpu")
def test_shape_mismatched_speaker_table_still_only_warns(tmp_path):
sd = TinyPlain().state_dict()
sd["embedding.weight"] = torch.zeros(20, 4)
path = _save_ckpt(tmp_path, sd, step=5)
assert load_checkpoint(path, TinyPlain(), device="cpu") == 5
def test_depth_arch_swap_is_allowed(tmp_path):
class GruDepth(nn.Module):
def __init__(self):
super().__init__()
self.depth_module = nn.Module()
self.depth_module.gru = nn.Linear(4, 4, bias=False)
self.depth_module.h_proj = nn.Linear(4, 4, bias=False)
class TfDepth(nn.Module):
def __init__(self):
super().__init__()
self.depth_module = nn.Module()
self.depth_module.depth_tf = nn.Module()
self.depth_module.depth_tf.self_attn = nn.Module()
self.depth_module.depth_tf.self_attn.out_proj = nn.Linear(4, 4, bias=False)
self.depth_module.h_proj = nn.Linear(4, 4, bias=False)
path = _save_ckpt(tmp_path, GruDepth().state_dict(), step=9)
assert load_checkpoint(path, TfDepth(), device="cpu") == 9
def test_gru_to_mamba2_depth_swap_is_allowed(tmp_path):
class GruDepth(nn.Module):
def __init__(self):
super().__init__()
self.depth_module = nn.Module()
self.depth_module.gru = nn.Linear(4, 4, bias=False)
class SsmDepth(nn.Module):
def __init__(self):
super().__init__()
self.depth_module = nn.Module()
self.depth_module.depth_ssm = nn.Module()
self.depth_module.depth_ssm.in_proj = nn.Linear(4, 4, bias=False)
self.depth_module.depth_ssm.out_proj = nn.Linear(4, 4, bias=False)
path = _save_ckpt(tmp_path, GruDepth().state_dict(), step=11)
assert load_checkpoint(path, SsmDepth(), device="cpu") == 11
def test_same_arch_missing_depth_projection_still_raises(tmp_path):
class TfDepth(nn.Module):
def __init__(self):
super().__init__()
self.depth_module = nn.Module()
self.depth_module.depth_tf = nn.Module()
self.depth_module.depth_tf.self_attn = nn.Module()
self.depth_module.depth_tf.self_attn.out_proj = nn.Linear(4, 4, bias=False)
sd = TfDepth().state_dict()
del sd["depth_module.depth_tf.self_attn.out_proj.weight"]
path = _save_ckpt(tmp_path, sd)
with pytest.raises(RuntimeError, match="received NO value"):
load_checkpoint(path, TfDepth(), device="cpu")
def test_matched_load_roundtrip_unaffected(tmp_path):
src = TinyLora()
path = _save_ckpt(tmp_path, src.state_dict(), step=7)
dst = TinyLora()
step = load_checkpoint(path, dst, device="cpu")
assert step == 7
torch.testing.assert_close(dst.in_proj.lora_A, src.in_proj.lora_A)
class TinyDelay(nn.Module):
def __init__(self):
super().__init__()
self.in_proj = nn.Linear(4, 8, bias=False)
self.delay_heads = nn.ModuleList([nn.Linear(8, 16, bias=False)])
class TinyDepth(nn.Module):
def __init__(self):
super().__init__()
self.in_proj = nn.Linear(4, 8, bias=False)
self.depth_gru = nn.GRU(8, 8, batch_first=True)
def test_delay_ckpt_into_depth_model_warm_start(tmp_path):
src = TinyDelay()
path = _save_ckpt(tmp_path, src.state_dict(), step=100000)
dst = TinyDepth()
before = {k: v.clone() for k, v in dst.state_dict().items()
if k.startswith("depth_gru")}
load_checkpoint(path, dst, device="cpu")
torch.testing.assert_close(dst.in_proj.weight, src.in_proj.weight)
for k, v in before.items():
torch.testing.assert_close(dst.state_dict()[k], v)
class TinySpeaker(nn.Module):
def __init__(self, num_speakers):
super().__init__()
self.speaker_encoder = nn.Module()
self.speaker_encoder.embedding = nn.Embedding(num_speakers, 4)
self.in_proj = nn.Linear(4, 8, bias=False)
def test_speaker_table_mismatch_is_a_hard_error(tmp_path):
path = _save_ckpt(tmp_path, TinySpeaker(16).state_dict())
with pytest.raises(RuntimeError, match=r"Speaker table.*--num_speakers 16"):
load_checkpoint(path, TinySpeaker(8), device="cpu")
def test_speaker_table_reinit_only_when_asked(tmp_path):
src = TinySpeaker(16)
with torch.no_grad():
src.in_proj.weight += 1.0
path = _save_ckpt(tmp_path, src.state_dict())
dst = TinySpeaker(8)
load_checkpoint(path, dst, device="cpu", allow_speaker_table_reinit=True)
torch.testing.assert_close(dst.in_proj.weight, src.in_proj.weight)
assert dst.speaker_encoder.embedding.weight.shape[0] == 8
def test_matching_speaker_table_loads_exactly(tmp_path):
src = TinySpeaker(16)
path = _save_ckpt(tmp_path, src.state_dict())
dst = TinySpeaker(16)
load_checkpoint(path, dst, device="cpu")
torch.testing.assert_close(dst.speaker_encoder.embedding.weight,
src.speaker_encoder.embedding.weight)
def test_checkpoint_speaker_rows_reads_the_table_size(tmp_path):
assert checkpoint_speaker_rows(
_save_ckpt(tmp_path, TinySpeaker(16).state_dict())) == 16
assert checkpoint_speaker_rows(
_save_ckpt(tmp_path, TinyPlain().state_dict(), name="plain.pt")) is None
assert checkpoint_speaker_rows(str(tmp_path / "absent.pt")) is None