test: cover MPS device and training paths

mengxin 2026-09-06 02:41:54 +08:00
parent fce15c0bf4
commit abfd81222a
4 changed files with 178 additions and 0 deletions

68
tests/benchmark_mps.py Normal file
View File

@ -0,0 +1,68 @@
"""Short MiniMind MPS training benchmark; it does not save checkpoints."""
import argparse
import os
import sys
import time
import torch
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from model.model_minimind import MiniMindConfig, MiniMindForCausalLM
from trainer.trainer_utils import get_autocast_context, get_grad_scaler
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--batch-size', type=int, default=8)
parser.add_argument('--seq-len', type=int, default=340)
parser.add_argument('--hidden-size', type=int, default=768)
parser.add_argument('--num-hidden-layers', type=int, default=8)
parser.add_argument('--dtype', choices=['float32', 'float16', 'bfloat16'], default='bfloat16')
parser.add_argument('--warmup-steps', type=int, default=1)
parser.add_argument('--steps', type=int, default=3)
args = parser.parse_args()
if not torch.backends.mps.is_available():
raise RuntimeError('MPS is not available')
config = MiniMindConfig(
hidden_size=args.hidden_size,
num_hidden_layers=args.num_hidden_layers,
max_position_embeddings=max(args.seq_len, 512),
)
model = MiniMindForCausalLM(config).to('mps').train()
optimizer = torch.optim.AdamW(model.parameters(), lr=5e-4)
scaler = get_grad_scaler('mps', args.dtype)
input_ids = torch.randint(
0, config.vocab_size, (args.batch_size, args.seq_len), device='mps'
)
durations = []
total_steps = args.warmup_steps + args.steps
for step in range(total_steps):
torch.mps.synchronize()
started = time.perf_counter()
with get_autocast_context('mps', args.dtype):
loss = model(input_ids, labels=input_ids).loss
scaler.scale(loss).backward()
scaler.unscale_(optimizer)
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
scaler.step(optimizer)
scaler.update()
optimizer.zero_grad(set_to_none=True)
torch.mps.synchronize()
elapsed = time.perf_counter() - started
if step >= args.warmup_steps:
durations.append(elapsed)
mean_seconds = sum(durations) / len(durations)
tokens_per_second = args.batch_size * args.seq_len / mean_seconds
print(f'mean_step_seconds={mean_seconds:.3f}')
print(f'tokens_per_second={tokens_per_second:.1f}')
print(f'loss={loss.detach().float().item():.4f}')
if __name__ == '__main__':
main()

8
tests/fixtures/pretrain_smoke.jsonl vendored Normal file
View File

@ -0,0 +1,8 @@
{"text":"苹果芯片可以通过 MPS 加速 PyTorch 模型训练。"}
{"text":"MiniMind 是一个用于学习语言模型训练流程的小型项目。"}
{"text":"梯度下降通过反向传播更新神经网络参数。"}
{"text":"混合精度训练可以降低内存占用并提高吞吐量。"}
{"text":"统一内存允许 CPU 和 GPU 共享同一块物理内存。"}
{"text":"检查点可用于恢复模型参数和优化器状态。"}
{"text":"注意力机制根据上下文计算不同词元之间的关系。"}
{"text":"小规模冒烟测试可以快速发现训练环境问题。"}

4
tests/fixtures/sft_smoke.jsonl vendored Normal file
View File

@ -0,0 +1,4 @@
{"conversations":[{"role":"user","content":"MPS 是什么?","reasoning_content":"","tools":"","tool_calls":""},{"role":"assistant","content":"MPS 是 Apple 平台上的 GPU 加速框架。","reasoning_content":"","tools":"","tool_calls":""}]}
{"conversations":[{"role":"user","content":"MiniMind 有什么用途?","reasoning_content":"","tools":"","tool_calls":""},{"role":"assistant","content":"它用于学习和实验小型语言模型训练流程。","reasoning_content":"","tools":"","tool_calls":""}]}
{"conversations":[{"role":"user","content":"什么是梯度?","reasoning_content":"","tools":"","tool_calls":""},{"role":"assistant","content":"梯度描述损失函数相对模型参数的变化方向。","reasoning_content":"","tools":"","tool_calls":""}]}
{"conversations":[{"role":"user","content":"为什么使用混合精度?","reasoning_content":"","tools":"","tool_calls":""},{"role":"assistant","content":"它通常能减少内存占用并提高训练速度。","reasoning_content":"","tools":"","tool_calls":""}]}

View File

@ -0,0 +1,98 @@
import os
import unittest
from unittest import mock
import torch
from model.model_minimind import MiniMindConfig, MiniMindForCausalLM
from trainer.trainer_utils import (
empty_device_cache,
get_autocast_context,
get_default_device,
get_grad_scaler,
init_model,
init_distributed_mode,
LMForRewardModel,
should_pin_memory,
)
class DeviceUtilsTest(unittest.TestCase):
def test_default_device_prefers_mps_over_cpu(self):
with mock.patch('torch.cuda.is_available', return_value=False), \
mock.patch('torch.backends.mps.is_available', return_value=True):
self.assertEqual(get_default_device(), 'mps')
def test_pin_memory_is_cuda_only(self):
self.assertTrue(should_pin_memory('cuda:0'))
self.assertFalse(should_pin_memory('mps'))
self.assertFalse(should_pin_memory('cpu'))
def test_distributed_mps_fails_with_actionable_message(self):
with mock.patch.dict(os.environ, {'RANK': '0'}):
with self.assertRaisesRegex(RuntimeError, 'single process'):
init_distributed_mode('mps')
@unittest.skipUnless(torch.backends.mps.is_available(), 'MPS is not available')
def test_init_model_uses_default_device_when_device_is_omitted(self):
config = MiniMindConfig(
hidden_size=64,
num_hidden_layers=2,
max_position_embeddings=64,
)
model, _ = init_model(
config,
from_weight='none',
tokenizer_path='model',
save_dir='out',
device=None,
)
self.assertEqual(next(model.parameters()).device.type, 'mps')
def test_reward_model_uses_default_device_when_device_is_omitted(self):
fake_model = mock.Mock()
fake_model.to.return_value = fake_model
fake_model.eval.return_value = fake_model
with mock.patch(
'trainer.trainer_utils.get_default_device', return_value='mps'
), mock.patch(
'trainer.trainer_utils.AutoTokenizer.from_pretrained'
), mock.patch(
'trainer.trainer_utils.AutoModel.from_pretrained', return_value=fake_model
):
LMForRewardModel('unused', device=None, dtype=torch.float32)
fake_model.to.assert_called_once_with('mps')
@unittest.skipUnless(torch.backends.mps.is_available(), 'MPS is not available')
def test_minimind_optimizer_step_on_mps_for_all_dtypes(self):
for dtype in ('float32', 'float16', 'bfloat16'):
with self.subTest(dtype=dtype):
config = MiniMindConfig(
hidden_size=64,
num_hidden_layers=2,
max_position_embeddings=64,
)
model = MiniMindForCausalLM(config).to('mps').train()
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)
scaler = get_grad_scaler('mps', dtype)
input_ids = torch.randint(0, config.vocab_size, (2, 32), device='mps')
with get_autocast_context('mps', dtype):
loss = model(input_ids, labels=input_ids).loss
self.assertTrue(torch.isfinite(loss).item())
scaler.scale(loss).backward()
scaler.unscale_(optimizer)
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
scaler.step(optimizer)
scaler.update()
optimizer.zero_grad(set_to_none=True)
torch.mps.synchronize()
empty_device_cache('mps')
if __name__ == '__main__':
unittest.main()