Download tests/test_train.py from lxcxjxhx/HOS-Model-Optimizer: direct link, hf CLI and curl.
- Browser
- Download file 20.6 kB
-
https://huggingface.co/lxcxjxhx/HOS-Model-Optimizer/resolve/main/tests/test_train.py
- Command line
-
hf download hf://lxcxjxhx/HOS-Model-Optimizer/tests/test_train.py
-
curl -L -o test_train.py https://huggingface.co/lxcxjxhx/HOS-Model-Optimizer/resolve/main/tests/test_train.py
20.6 kB
| """ | |
| 训练模块单元测试 | |
| 测试训练模块的所有功能,包括: | |
| - QLoRA 配置 | |
| - LoRA 配置 | |
| - 数据集加载和处理 | |
| - 模型合并 | |
| - 训练流程 | |
| 使用 mock 避免实际训练和模型加载。 | |
| """ | |
| import os | |
| import sys | |
| import pytest | |
| from pathlib import Path | |
| from unittest.mock import MagicMock, patch, Mock | |
| import json | |
| # 添加项目路径 | |
| sys.path.insert(0, str(Path(__file__).parent.parent)) | |
| from hos_optimizer.train import ( | |
| TrainingConfig, | |
| DatasetProcessor, | |
| load_and_process_dataset, | |
| get_quantization_config, | |
| load_model_and_tokenizer, | |
| get_lora_config, | |
| prepare_model_for_training, | |
| get_training_arguments, | |
| VRAMCallback, | |
| train, | |
| merge_model, | |
| ) | |
| class TestTrainingConfig: | |
| """训练配置测试""" | |
| def test_default_config(self): | |
| """测试默认配置""" | |
| config = TrainingConfig() | |
| assert config.model_name_or_path == "Qwen/Qwen2.5-0.5B" | |
| assert config.finetuning_type == "qlora" | |
| assert config.use_4bit is True | |
| assert config.lora_rank == 16 | |
| assert config.lora_alpha == 32 | |
| assert config.num_train_epochs == 3 | |
| assert config.learning_rate == 2e-4 | |
| def test_custom_config(self): | |
| """测试自定义配置""" | |
| config = TrainingConfig( | |
| model_name_or_path="custom/model", | |
| finetuning_type="lora", | |
| use_4bit=False, | |
| lora_rank=32, | |
| lora_alpha=64, | |
| num_train_epochs=5, | |
| learning_rate=1e-4 | |
| ) | |
| assert config.model_name_or_path == "custom/model" | |
| assert config.finetuning_type == "lora" | |
| assert config.use_4bit is False | |
| assert config.lora_rank == 32 | |
| assert config.lora_alpha == 64 | |
| assert config.num_train_epochs == 5 | |
| assert config.learning_rate == 1e-4 | |
| class TestDatasetProcessor: | |
| """数据集处理器测试""" | |
| def test_format_alpaca_with_input(self): | |
| """测试 Alpaca 格式带输入的处理""" | |
| mock_tokenizer = MagicMock() | |
| processor = DatasetProcessor(mock_tokenizer, max_seq_length=512) | |
| example = { | |
| "instruction": "什么是网络安全?", | |
| "input": "请举例说明", | |
| "output": "网络安全是指保护计算机网络免受未经授权的访问。" | |
| } | |
| result = processor.format_alpaca(example) | |
| assert "### 指令:" in result["text"] | |
| assert "什么是网络安全?" in result["text"] | |
| assert "### 输入:" in result["text"] | |
| assert "请举例说明" in result["text"] | |
| assert "### 回答:" in result["text"] | |
| assert "保护计算机网络" in result["text"] | |
| def test_format_alpaca_without_input(self): | |
| """测试 Alpaca 格式不带输入的处理""" | |
| mock_tokenizer = MagicMock() | |
| processor = DatasetProcessor(mock_tokenizer, max_seq_length=512) | |
| example = { | |
| "instruction": "什么是网络安全?", | |
| "input": "", | |
| "output": "网络安全是指保护计算机网络。" | |
| } | |
| result = processor.format_alpaca(example) | |
| assert "### 指令:" in result["text"] | |
| assert "什么是网络安全?" in result["text"] | |
| assert "### 输入:" not in result["text"] | |
| assert "### 回答:" in result["text"] | |
| def test_format_sharegpt(self): | |
| """测试 ShareGPT 格式的处理""" | |
| mock_tokenizer = MagicMock() | |
| processor = DatasetProcessor(mock_tokenizer, max_seq_length=512) | |
| example = { | |
| "conversations": [ | |
| {"from": "human", "value": "你好"}, | |
| {"from": "gpt", "value": "你好!有什么可以帮助你的吗?"}, | |
| ] | |
| } | |
| result = processor.format_sharegpt(example) | |
| assert "### 用户:" in result["text"] | |
| assert "你好" in result["text"] | |
| assert "### 助手:" in result["text"] | |
| assert "帮助你的吗" in result["text"] | |
| def test_tokenize_function(self): | |
| """测试分词函数""" | |
| mock_tokenizer = MagicMock() | |
| mock_tokenizer.return_value = { | |
| "input_ids": [1, 2, 3, 4, 5], | |
| "attention_mask": [1, 1, 1, 1, 1] | |
| } | |
| processor = DatasetProcessor(mock_tokenizer, max_seq_length=512) | |
| example = {"text": "测试文本"} | |
| result = processor.tokenize_function(example) | |
| assert "input_ids" in result | |
| assert "attention_mask" in result | |
| assert "labels" in result | |
| assert result["labels"] == result["input_ids"] | |
| def test_process_dataset_alpaca(self): | |
| """测试处理 Alpaca 格式数据集""" | |
| mock_tokenizer = MagicMock() | |
| mock_tokenizer.return_value = { | |
| "input_ids": [1, 2, 3], | |
| "attention_mask": [1, 1, 1] | |
| } | |
| mock_tokenizer.num_proc = 4 | |
| processor = DatasetProcessor(mock_tokenizer, max_seq_length=512) | |
| mock_dataset = MagicMock() | |
| mock_dataset.__len__.return_value = 10 | |
| mock_dataset.column_names = ["instruction", "input", "output"] | |
| # Mock map 方法 | |
| def mock_map(func, **kwargs): | |
| return mock_dataset | |
| mock_dataset.map = mock_map | |
| result = processor.process_dataset(mock_dataset, dataset_format="alpaca") | |
| assert result is not None | |
| def test_process_dataset_invalid_format(self): | |
| """测试处理无效格式数据集""" | |
| mock_tokenizer = MagicMock() | |
| processor = DatasetProcessor(mock_tokenizer, max_seq_length=512) | |
| mock_dataset = MagicMock() | |
| with pytest.raises(ValueError) as exc_info: | |
| processor.process_dataset(mock_dataset, dataset_format="invalid_format") | |
| assert "不支持的数据格式" in str(exc_info.value) | |
| class TestLoadAndProcessDataset: | |
| """数据集加载和处理测试""" | |
| def test_load_dataset_file_not_found(self): | |
| """测试加载不存在的数据集文件""" | |
| mock_tokenizer = MagicMock() | |
| with pytest.raises(FileNotFoundError) as exc_info: | |
| load_and_process_dataset( | |
| dataset_path="/nonexistent/dataset.json", | |
| tokenizer=mock_tokenizer | |
| ) | |
| assert "数据集文件不存在" in str(exc_info.value) | |
| def test_load_dataset_success(self, tmp_dir): | |
| """测试成功加载数据集""" | |
| # 创建测试数据集 | |
| dataset_path = os.path.join(tmp_dir, "dataset.json") | |
| data = [ | |
| { | |
| "instruction": "测试指令", | |
| "input": "", | |
| "output": "测试输出" | |
| } | |
| ] | |
| with open(dataset_path, "w", encoding="utf-8") as f: | |
| json.dump(data, f, ensure_ascii=False) | |
| mock_tokenizer = MagicMock() | |
| mock_tokenizer.return_value = { | |
| "input_ids": [1, 2, 3], | |
| "attention_mask": [1, 1, 1] | |
| } | |
| mock_tokenizer.num_proc = 4 | |
| with patch("hos_optimizer.train.load_dataset") as mock_load: | |
| mock_dataset = MagicMock() | |
| mock_dataset.__len__.return_value = 10 | |
| mock_dataset.column_names = ["instruction", "input", "output"] | |
| mock_dataset.map.return_value = mock_dataset | |
| mock_dataset.train_test_split.return_value = { | |
| "train": MagicMock(__len__=MagicMock(return_value=9)), | |
| "test": MagicMock(__len__=MagicMock(return_value=1)) | |
| } | |
| mock_load.return_value = mock_dataset | |
| result = load_and_process_dataset( | |
| dataset_path=dataset_path, | |
| tokenizer=mock_tokenizer, | |
| dataset_format="alpaca", | |
| max_seq_length=512, | |
| test_size=0.1 | |
| ) | |
| assert "train" in result | |
| assert "test" in result | |
| class TestQuantizationConfig: | |
| """量化配置测试""" | |
| def test_get_quantization_config_disabled(self): | |
| """测试禁用量化配置""" | |
| config = TrainingConfig(use_4bit=False) | |
| result = get_quantization_config(config) | |
| assert result is None | |
| def test_get_quantization_config_enabled(self): | |
| """测试启用量化配置""" | |
| config = TrainingConfig( | |
| use_4bit=True, | |
| bnb_4bit_quant_type="nf4", | |
| bnb_4bit_use_double_quant=True, | |
| bnb_4bit_compute_dtype="bfloat16" | |
| ) | |
| with patch("hos_optimizer.train.BitsAndBytesConfig") as mock_bnb: | |
| mock_config = MagicMock() | |
| mock_bnb.return_value = mock_config | |
| result = get_quantization_config(config) | |
| assert result is not None | |
| mock_bnb.assert_called_once() | |
| class TestLoadModelAndTokenizer: | |
| """模型和分词器加载测试""" | |
| def test_load_model_standard(self): | |
| """测试标准模型加载""" | |
| config = TrainingConfig( | |
| model_name_or_path="test/model", | |
| use_unsloth=False, | |
| use_4bit=False | |
| ) | |
| with patch("hos_optimizer.train.AutoTokenizer") as mock_tokenizer_cls, \ | |
| patch("hos_optimizer.train.AutoModelForCausalLM") as mock_model_cls: | |
| mock_tokenizer = MagicMock() | |
| mock_tokenizer.pad_token = None | |
| mock_tokenizer.eos_token = "<eos>" | |
| mock_tokenizer_cls.from_pretrained.return_value = mock_tokenizer | |
| mock_model = MagicMock() | |
| mock_model.num_parameters.return_value = 1e9 | |
| mock_model_cls.from_pretrained.return_value = mock_model | |
| model, tokenizer = load_model_and_tokenizer(config) | |
| assert model is not None | |
| assert tokenizer is not None | |
| assert tokenizer.pad_token == "<eos>" | |
| def test_load_model_with_unsloth(self): | |
| """测试使用 Unsloth 加载模型""" | |
| config = TrainingConfig( | |
| model_name_or_path="test/model", | |
| use_unsloth=True, | |
| finetuning_type="qlora" | |
| ) | |
| with patch("hos_optimizer.train.UNSLOTH_AVAILABLE", True), \ | |
| patch("hos_optimizer.train.FastLanguageModel") as mock_fast: | |
| mock_model = MagicMock() | |
| mock_tokenizer = MagicMock() | |
| mock_fast.from_pretrained.return_value = (mock_model, mock_tokenizer) | |
| model, tokenizer = load_model_and_tokenizer(config) | |
| assert model is not None | |
| assert tokenizer is not None | |
| mock_fast.from_pretrained.assert_called_once() | |
| def test_load_model_with_quantization(self): | |
| """测试带量化的模型加载""" | |
| config = TrainingConfig( | |
| model_name_or_path="test/model", | |
| use_unsloth=False, | |
| use_4bit=True | |
| ) | |
| with patch("hos_optimizer.train.get_quantization_config") as mock_get_quant, \ | |
| patch("hos_optimizer.train.AutoTokenizer") as mock_tokenizer_cls, \ | |
| patch("hos_optimizer.train.AutoModelForCausalLM") as mock_model_cls: | |
| mock_quant_config = MagicMock() | |
| mock_get_quant.return_value = mock_quant_config | |
| mock_tokenizer = MagicMock() | |
| mock_tokenizer.pad_token = "<pad>" | |
| mock_tokenizer_cls.from_pretrained.return_value = mock_tokenizer | |
| mock_model = MagicMock() | |
| mock_model.num_parameters.return_value = 1e9 | |
| mock_model_cls.from_pretrained.return_value = mock_model | |
| model, tokenizer = load_model_and_tokenizer(config) | |
| assert model is not None | |
| # 验证量化配置被传递 | |
| call_kwargs = mock_model_cls.from_pretrained.call_args[1] | |
| assert "quantization_config" in call_kwargs | |
| class TestLoraConfig: | |
| """LoRA 配置测试""" | |
| def test_get_lora_config_all_modules(self): | |
| """测试所有模块的 LoRA 配置""" | |
| config = TrainingConfig( | |
| lora_rank=16, | |
| lora_alpha=32, | |
| lora_dropout=0.05, | |
| lora_target_modules=["all"] | |
| ) | |
| with patch("hos_optimizer.train.LoraConfig") as mock_lora: | |
| mock_config = MagicMock() | |
| mock_lora.return_value = mock_config | |
| result = get_lora_config(config) | |
| assert result is not None | |
| mock_lora.assert_called_once() | |
| # 验证 target_modules 为 None(表示所有模块) | |
| call_kwargs = mock_lora.call_args[1] | |
| assert call_kwargs["target_modules"] is None | |
| def test_get_lora_config_specific_modules(self): | |
| """测试特定模块的 LoRA 配置""" | |
| config = TrainingConfig( | |
| lora_rank=16, | |
| lora_alpha=32, | |
| lora_dropout=0.05, | |
| lora_target_modules=["q_proj", "v_proj"] | |
| ) | |
| with patch("hos_optimizer.train.LoraConfig") as mock_lora: | |
| mock_config = MagicMock() | |
| mock_lora.return_value = mock_config | |
| result = get_lora_config(config) | |
| assert result is not None | |
| call_kwargs = mock_lora.call_args[1] | |
| assert call_kwargs["target_modules"] == ["q_proj", "v_proj"] | |
| class TestPrepareModelForTraining: | |
| """模型训练准备测试""" | |
| def test_prepare_model_qlora(self): | |
| """测试 QLoRA 模式的模型准备""" | |
| config = TrainingConfig( | |
| use_4bit=True, | |
| finetuning_type="qlora" | |
| ) | |
| mock_model = MagicMock() | |
| with patch("hos_optimizer.train.prepare_model_for_kbit_training") as mock_prepare, \ | |
| patch("hos_optimizer.train.get_lora_config") as mock_get_lora, \ | |
| patch("hos_optimizer.train.get_peft_model") as mock_get_peft: | |
| mock_prepare.return_value = mock_model | |
| mock_lora_config = MagicMock() | |
| mock_get_lora.return_value = mock_lora_config | |
| mock_get_peft.return_value = mock_model | |
| result = prepare_model_for_training(mock_model, config) | |
| assert result is not None | |
| mock_prepare.assert_called_once() | |
| mock_get_peft.assert_called_once() | |
| def test_prepare_model_lora(self): | |
| """测试 LoRA 模式的模型准备""" | |
| config = TrainingConfig( | |
| use_4bit=False, | |
| finetuning_type="lora" | |
| ) | |
| mock_model = MagicMock() | |
| with patch("hos_optimizer.train.get_lora_config") as mock_get_lora, \ | |
| patch("hos_optimizer.train.get_peft_model") as mock_get_peft: | |
| mock_lora_config = MagicMock() | |
| mock_get_lora.return_value = mock_lora_config | |
| mock_get_peft.return_value = mock_model | |
| result = prepare_model_for_training(mock_model, config) | |
| assert result is not None | |
| mock_get_peft.assert_called_once() | |
| class TestTrainingArguments: | |
| """训练参数测试""" | |
| def test_get_training_arguments(self): | |
| """测试获取训练参数""" | |
| config = TrainingConfig( | |
| output_dir="./test_output", | |
| num_train_epochs=5, | |
| per_device_train_batch_size=4, | |
| learning_rate=1e-4 | |
| ) | |
| with patch("hos_optimizer.train.TrainingArguments") as mock_args: | |
| mock_training_args = MagicMock() | |
| mock_args.return_value = mock_training_args | |
| result = get_training_arguments(config) | |
| assert result is not None | |
| mock_args.assert_called_once() | |
| class TestVRAMCallback: | |
| """VRAM 回调测试""" | |
| def test_vram_callback_on_log(self): | |
| """测试 VRAM 回调的日志记录""" | |
| callback = VRAMCallback() | |
| mock_args = MagicMock() | |
| mock_state = MagicMock() | |
| mock_state.global_step = 100 | |
| mock_state.is_world_process_zero = True | |
| mock_control = MagicMock() | |
| logs = {} | |
| with patch("torch.cuda.is_available", return_value=True), \ | |
| patch("torch.cuda.max_memory_allocated", return_value=4 * 1024 ** 3), \ | |
| patch("torch.cuda.memory_reserved", return_value=6 * 1024 ** 3): | |
| callback.on_log(mock_args, mock_state, mock_control, logs=logs) | |
| assert "gpu_memory_gb" in logs | |
| assert "gpu_memory_reserved_gb" in logs | |
| class TestTrain: | |
| """训练流程测试""" | |
| def test_train_success(self, tmp_dir): | |
| """测试成功训练流程""" | |
| # 创建测试数据集 | |
| dataset_path = os.path.join(tmp_dir, "dataset.json") | |
| data = [{"instruction": "测试", "input": "", "output": "输出"}] | |
| with open(dataset_path, "w", encoding="utf-8") as f: | |
| json.dump(data, f, ensure_ascii=False) | |
| config = TrainingConfig( | |
| model_name_or_path="test/model", | |
| dataset_path=dataset_path, | |
| output_dir=os.path.join(tmp_dir, "output") | |
| ) | |
| with patch("hos_optimizer.train.load_model_and_tokenizer") as mock_load, \ | |
| patch("hos_optimizer.train.load_and_process_dataset") as mock_load_dataset, \ | |
| patch("hos_optimizer.train.prepare_model_for_training") as mock_prepare, \ | |
| patch("hos_optimizer.train.get_training_arguments") as mock_get_args, \ | |
| patch("hos_optimizer.train.Trainer") as mock_trainer_cls, \ | |
| patch("hos_optimizer.train.DataCollatorForSeq2Seq"): | |
| mock_model = MagicMock() | |
| mock_tokenizer = MagicMock() | |
| mock_load.return_value = (mock_model, mock_tokenizer) | |
| mock_dataset_dict = { | |
| "train": MagicMock(), | |
| "test": MagicMock() | |
| } | |
| mock_load_dataset.return_value = mock_dataset_dict | |
| mock_prepare.return_value = mock_model | |
| mock_training_args = MagicMock() | |
| mock_get_args.return_value = mock_training_args | |
| mock_trainer = MagicMock() | |
| mock_train_result = MagicMock() | |
| mock_train_result.training_loss = 0.5 | |
| mock_train_result.metrics = {"train_runtime": 100.0} | |
| mock_trainer.train.return_value = mock_train_result | |
| mock_trainer_cls.return_value = mock_trainer | |
| train(config) | |
| mock_trainer.train.assert_called_once() | |
| mock_trainer.save_model.assert_called_once() | |
| class TestMergeModel: | |
| """模型合并测试""" | |
| def test_merge_model_success(self, tmp_dir): | |
| """测试成功合并模型""" | |
| base_model_path = "/path/to/base" | |
| adapter_path = "/path/to/adapter" | |
| output_path = os.path.join(tmp_dir, "merged") | |
| with patch("hos_optimizer.train.AutoModelForCausalLM") as mock_model_cls, \ | |
| patch("hos_optimizer.train.PeftModel") as mock_peft, \ | |
| patch("hos_optimizer.train.AutoTokenizer") as mock_tokenizer_cls, \ | |
| patch("os.makedirs"): | |
| mock_base_model = MagicMock() | |
| mock_model_cls.from_pretrained.return_value = mock_base_model | |
| mock_peft_model = MagicMock() | |
| mock_merged_model = MagicMock() | |
| mock_peft.from_pretrained.return_value = mock_peft_model | |
| mock_peft_model.merge_and_unload.return_value = mock_merged_model | |
| mock_tokenizer = MagicMock() | |
| mock_tokenizer_cls.from_pretrained.return_value = mock_tokenizer | |
| merge_model( | |
| base_model_path=base_model_path, | |
| adapter_path=adapter_path, | |
| output_path=output_path | |
| ) | |
| mock_peft_model.merge_and_unload.assert_called_once() | |
| mock_merged_model.save_pretrained.assert_called_once_with(output_path) | |
| mock_tokenizer.save_pretrained.assert_called_once_with(output_path) | |
| if __name__ == "__main__": | |
| pytest.main([__file__, "-v"]) | |