minimind/scripts/smoke_test.py

152 lines
5.8 KiB
Python

"""
Lightweight sanity checks for MiniMind development.
This script intentionally uses tiny random models, so it can run on CPU before
launching expensive DDP jobs on the training server.
Examples:
python scripts/smoke_test.py
python scripts/smoke_test.py --device cuda:0 --hidden_size 128 --seq_len 32
"""
import argparse
import os
import sys
import tempfile
import torch
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from model.model_minimind import MiniMindConfig, MiniMindForCausalLM
from model.model_minimind_mla import MiniMindMLAConfig, MiniMindMLAForCausalLM
from trainer.trainer_utils import lm_checkpoint
def assert_shape(name, actual, expected):
if tuple(actual) != tuple(expected):
raise AssertionError(f"{name} shape mismatch: got {tuple(actual)}, expected {tuple(expected)}")
def build_config(attention_type, args):
common = dict(
hidden_size=args.hidden_size,
num_hidden_layers=args.num_hidden_layers,
vocab_size=args.vocab_size,
max_position_embeddings=max(args.seq_len + args.max_new_tokens + 8, 64),
num_attention_heads=args.num_attention_heads,
dropout=0.0,
flash_attn=args.flash_attn,
)
if attention_type == "mla":
return MiniMindMLAConfig(kv_lora_rank=args.kv_lora_rank, **common)
return MiniMindConfig(attention_type=attention_type, **common)
def build_model(config):
if isinstance(config, MiniMindMLAConfig):
return MiniMindMLAForCausalLM(config)
return MiniMindForCausalLM(config)
def check_forward_and_cache(attention_type, args):
config = build_config(attention_type, args)
model = build_model(config).to(args.device).eval()
input_ids = torch.randint(3, args.vocab_size, (args.batch_size, args.seq_len), device=args.device)
attention_mask = torch.ones_like(input_ids)
with torch.no_grad():
out = model(input_ids, attention_mask=attention_mask, labels=input_ids, use_cache=True)
assert out.loss is not None, f"{attention_type}: loss is None"
assert_shape(f"{attention_type}: logits", out.logits.shape, (args.batch_size, args.seq_len, args.vocab_size))
if len(out.past_key_values) != args.num_hidden_layers:
raise AssertionError(f"{attention_type}: unexpected cache layers={len(out.past_key_values)}")
first_cache = out.past_key_values[0]
if attention_type == "mla":
kv_latent, k_rope = first_cache
assert_shape("mla: kv_latent", kv_latent.shape, (args.batch_size, args.seq_len, args.kv_lora_rank))
assert_shape("mla: k_rope", k_rope.shape, (args.batch_size, args.seq_len, config.rope_dim))
cache_floats = args.kv_lora_rank + config.rope_dim
else:
key, value = first_cache
assert_shape(
f"{attention_type}: key",
key.shape,
(args.batch_size, args.seq_len, config.num_key_value_heads, config.head_dim),
)
assert_shape(
f"{attention_type}: value",
value.shape,
(args.batch_size, args.seq_len, config.num_key_value_heads, config.head_dim),
)
cache_floats = 2 * config.num_key_value_heads * config.head_dim
with torch.no_grad():
generated = model.generate(
input_ids=input_ids,
attention_mask=attention_mask,
max_new_tokens=args.max_new_tokens,
do_sample=False,
top_k=0,
eos_token_id=None,
use_cache=True,
)
assert_shape(
f"{attention_type}: generated",
generated.shape,
(args.batch_size, args.seq_len + args.max_new_tokens),
)
params = sum(p.numel() for p in model.parameters())
print(
f"[OK] {attention_type:<3} params={params / 1e6:.3f}M "
f"cache={cache_floats} floats/token/layer loss={out.loss.item():.4f}"
)
return model, config
def check_checkpoint_roundtrip(args):
config = build_config("gqa", args)
model = build_model(config).to(args.device)
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)
with tempfile.TemporaryDirectory() as tmpdir:
lm_checkpoint(config, weight="smoke", model=model, optimizer=optimizer, epoch=1, step=3, save_dir=tmpdir)
loaded = lm_checkpoint(config, weight="smoke", save_dir=tmpdir)
if loaded is None:
raise AssertionError("checkpoint roundtrip failed: no resume data")
if loaded.get("epoch") != 1 or loaded.get("step") != 3:
raise AssertionError(f"checkpoint metadata mismatch: {loaded.get('epoch')=}, {loaded.get('step')=}")
print("[OK] checkpoint save/load roundtrip")
def main():
parser = argparse.ArgumentParser(description="MiniMind smoke tests")
parser.add_argument("--device", default="cuda:0" if torch.cuda.is_available() else "cpu")
parser.add_argument("--batch_size", type=int, default=2)
parser.add_argument("--seq_len", type=int, default=16)
parser.add_argument("--max_new_tokens", type=int, default=4)
parser.add_argument("--hidden_size", type=int, default=64)
parser.add_argument("--num_hidden_layers", type=int, default=2)
parser.add_argument("--num_attention_heads", type=int, default=8)
parser.add_argument("--vocab_size", type=int, default=128)
parser.add_argument("--kv_lora_rank", type=int, default=16)
parser.add_argument("--flash_attn", action="store_true")
parser.add_argument("--skip_checkpoint", action="store_true")
args = parser.parse_args()
if args.hidden_size % args.num_attention_heads != 0:
raise ValueError("hidden_size must be divisible by num_attention_heads")
torch.manual_seed(42)
for attention_type in ("mha", "gqa", "mqa", "mla"):
check_forward_and_cache(attention_type, args)
if not args.skip_checkpoint:
check_checkpoint_roundtrip(args)
print("All smoke tests passed.")
if __name__ == "__main__":
main()