152 lines
5.8 KiB
Python
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()
|