lxcxjxhx commited on
Commit
6099266
·
verified ·
1 Parent(s): dcacc23

Upload tests/test_quantize.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. tests/test_quantize.py +476 -0
tests/test_quantize.py ADDED
@@ -0,0 +1,476 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 量化模块单元测试
3
+
4
+ 测试量化模块的所有功能,包括:
5
+ - GGUF 量化
6
+ - AWQ 量化
7
+ - GPTQ 量化
8
+ - PPL 评估
9
+ - 格式转换
10
+ - VRAM 检查和优化
11
+
12
+ 使用 mock 避免实际模型加载和量化过程。
13
+ """
14
+
15
+ import os
16
+ import sys
17
+ import pytest
18
+ from pathlib import Path
19
+ from unittest.mock import MagicMock, patch, Mock
20
+ import subprocess
21
+
22
+ # 添加项目路径
23
+ sys.path.insert(0, str(Path(__file__).parent.parent))
24
+
25
+ from hos_optimizer.quantize import (
26
+ QuantizationError,
27
+ check_vram_availability,
28
+ optimize_for_low_vram,
29
+ quantize_gguf,
30
+ quantize_awq,
31
+ quantize_gptq,
32
+ evaluate_perplexity,
33
+ convert_format,
34
+ get_model_size,
35
+ VRAM_8GB_CONFIG,
36
+ )
37
+
38
+
39
+ class TestVRAMCheck:
40
+ """VRAM 检查相关测试"""
41
+
42
+ def test_check_vram_no_cuda(self):
43
+ """测试无 CUDA 支持时的 VRAM 检查"""
44
+ with patch("torch.cuda.is_available", return_value=False):
45
+ result = check_vram_availability()
46
+
47
+ assert result["available"] is False
48
+ assert result["total_vram_gb"] == 0
49
+ assert result["free_vram_gb"] == 0
50
+ assert result["device"] == "cpu"
51
+
52
+ def test_check_vram_with_cuda(self):
53
+ """测试有 CUDA 支持时的 VRAM 检查"""
54
+ mock_device = MagicMock()
55
+ mock_device.total_memory = 8 * 1024 ** 3 # 8GB
56
+
57
+ with patch("torch.cuda.is_available", return_value=True), \
58
+ patch("torch.cuda.current_device", return_value=0), \
59
+ patch("torch.cuda.get_device_properties", return_value=mock_device), \
60
+ patch("torch.cuda.memory_allocated", return_value=2 * 1024 ** 3), \
61
+ patch("torch.cuda.get_device_name", return_value="Test GPU"):
62
+
63
+ result = check_vram_availability()
64
+
65
+ assert result["available"] is True
66
+ assert result["total_vram_gb"] == 8.0
67
+ assert result["free_vram_gb"] == 6.0
68
+ assert result["device"] == "Test GPU"
69
+
70
+ def test_optimize_for_low_vram_enabled(self):
71
+ """测试低 VRAM 优化配置启用用的情况"""
72
+ config = {"batch_size": 4, "seq_length": 1024}
73
+
74
+ with patch("hos_optimizer.quantize.check_vram_availability") as mock_check:
75
+ mock_check.return_value = {
76
+ "available": True,
77
+ "free_vram_gb": 6.0,
78
+ "total_vram_gb": 8.0,
79
+ "device": "Test GPU"
80
+ }
81
+
82
+ optimized = optimize_for_low_vram(config)
83
+
84
+ # 应该应用 8GB 优化配置
85
+ assert optimized["max_batch_size"] == VRAM_8GB_CONFIG["max_batch_size"]
86
+ assert optimized["max_seq_length"] == VRAM_8GB_CONFIG["max_seq_length"]
87
+ assert optimized["gradient_checkpointing"] is True
88
+ assert optimized["offload_to_cpu"] is True
89
+ # 原有配置应该保留
90
+ assert optimized["batch_size"] == 4
91
+ assert optimized["seq_length"] == 1024
92
+
93
+ def test_optimize_for_low_vram_disabled(self):
94
+ """测试低 VRAM 优化配置不适用的情况"""
95
+ config = {"batch_size": 4, "seq_length": 1024}
96
+
97
+ with patch("hos_optimizer.quantize.check_vram_availability") as mock_check:
98
+ mock_check.return_value = {
99
+ "available": True,
100
+ "free_vram_gb": 12.0, # 超过 8GB
101
+ "total_vram_gb": 16.0,
102
+ "device": "Test GPU"
103
+ }
104
+
105
+ optimized = optimize_for_low_vram(config)
106
+
107
+ # 不应该应用优化配置
108
+ assert optimized == config
109
+
110
+
111
+ class TestGGUFQuantization:
112
+ """GGUF 量化测试"""
113
+
114
+ def test_quantize_gguf_success(self, tmp_dir):
115
+ """测试 GGUF 量化成功场景"""
116
+ model_path = os.path.join(tmp_dir, "model")
117
+ output_path = os.path.join(tmp_dir, "model.gguf")
118
+ llama_cpp_path = "/path/to/llama.cpp"
119
+
120
+ # Mock 所有依赖
121
+ with patch("subprocess.run") as mock_run, \
122
+ patch("hos_optimizer.quantize.AutoModelForCausalLM") as mock_model_cls, \
123
+ patch("hos_optimizer.quantize.AutoTokenizer") as mock_tokenizer_cls, \
124
+ patch("tempfile.TemporaryDirectory") as mock_tmpdir:
125
+
126
+ # Mock subprocess 调用
127
+ mock_run.return_value = MagicMock(returncode=0)
128
+
129
+ # Mock 临时目录
130
+ mock_tmpdir.return_value.__enter__.return_value = tmp_dir
131
+
132
+ # Mock 模型和分词器
133
+ mock_model = MagicMock()
134
+ mock_tokenizer = MagicMock()
135
+ mock_model_cls.from_pretrained.return_value = mock_model
136
+ mock_tokenizer_cls.from_pretrained.return_value = mock_tokenizer
137
+
138
+ result = quantize_gguf(
139
+ model_path=model_path,
140
+ output_path=output_path,
141
+ quant_type="Q4_K_M",
142
+ llama_cpp_path=llama_cpp_path
143
+ )
144
+
145
+ assert result == output_path
146
+ # 验证调用了转换和量化命令
147
+ assert mock_run.call_count >= 2
148
+
149
+ def test_quantize_gguf_tool_not_found(self, tmp_dir):
150
+ """测试 GGUF 量化工具不存在的情况"""
151
+ model_path = os.path.join(tmp_dir, "model")
152
+ output_path = os.path.join(tmp_dir, "model.gguf")
153
+
154
+ with patch("subprocess.run") as mock_run:
155
+ mock_run.side_effect = FileNotFoundError()
156
+
157
+ with pytest.raises(QuantizationError) as exc_info:
158
+ quantize_gguf(model_path, output_path)
159
+
160
+ assert "找不到 llama-quantize 工具" in str(exc_info.value)
161
+
162
+ def test_quantize_gguf_conversion_failed(self, tmp_dir):
163
+ """测试 GGUF 量化转换失败的情况"""
164
+ model_path = os.path.join(tmp_dir, "model")
165
+ output_path = os.path.join(tmp_dir, "model.gguf")
166
+
167
+ with patch("subprocess.run") as mock_run, \
168
+ patch("hos_optimizer.quantize.AutoModelForCausalLM") as mock_model_cls, \
169
+ patch("hos_optimizer.quantize.AutoTokenizer") as mock_tokenizer_cls, \
170
+ patch("tempfile.TemporaryDirectory") as mock_tmpdir:
171
+
172
+ # 第一次调用成功(检查工具),第二次调用失败(转换)
173
+ mock_run.side_effect = [
174
+ MagicMock(returncode=0), # 检查工具
175
+ subprocess.CalledProcessError(1, "convert", stderr="Conversion failed")
176
+ ]
177
+
178
+ mock_tmpdir.return_value.__enter__.return_value = tmp_dir
179
+ mock_model_cls.from_pretrained.return_value = MagicMock()
180
+ mock_tokenizer_cls.from_pretrained.return_value = MagicMock()
181
+
182
+ with pytest.raises(QuantizationError) as exc_info:
183
+ quantize_gguf(model_path, output_path)
184
+
185
+ assert "GGUF 量化失败" in str(exc_info.value)
186
+
187
+
188
+ class TestAWQQuantization:
189
+ """AWQ 量化测试"""
190
+
191
+ def test_quantize_awq_success(self, tmp_dir):
192
+ """测试 AWQ 量化成功场景"""
193
+ model_path = os.path.join(tmp_dir, "model")
194
+ output_path = os.path.join(tmp_dir, "model-awq")
195
+
196
+ with patch("hos_optimizer.quantize.AutoAWQForCausalLM") as mock_awq_cls, \
197
+ patch("hos_optimizer.quantize.AutoTokenizer") as mock_tokenizer_cls, \
198
+ patch("hos_optimizer.quantize.optimize_for_low_vram") as mock_optimize:
199
+
200
+ mock_model = MagicMock()
201
+ mock_tokenizer = MagicMock()
202
+ mock_awq_cls.from_pretrained.return_value = mock_model
203
+ mock_tokenizer_cls.from_pretrained.return_value = mock_tokenizer
204
+ mock_optimize.return_value = {
205
+ "zero_point": True,
206
+ "q_group_size": 128,
207
+ "w_bit": 4,
208
+ "version": "GEMM"
209
+ }
210
+
211
+ result = quantize_awq(
212
+ model_path=model_path,
213
+ output_path=output_path,
214
+ bits=4,
215
+ group_size=128
216
+ )
217
+
218
+ assert result == output_path
219
+ mock_model.quantize.assert_called_once()
220
+ mock_model.save_quantized.assert_called_once_with(output_path)
221
+ mock_tokenizer.save_pretrained.assert_called_once_with(output_path)
222
+
223
+ def test_quantize_awq_missing_dependency(self, tmp_dir):
224
+ """测试 AWQ 量化缺少依赖的情况"""
225
+ model_path = os.path.join(tmp_dir, "model")
226
+ output_path = os.path.join(tmp_dir, "model-awq")
227
+
228
+ with patch("hos_optimizer.quantize.AutoAWQForCausalLM") as mock_awq_cls:
229
+ mock_awq_cls.from_pretrained.side_effect = ImportError("autoawq")
230
+
231
+ with pytest.raises(QuantizationError) as exc_info:
232
+ quantize_awq(model_path, output_path)
233
+
234
+ assert "缺少依赖" in str(exc_info.value)
235
+ assert "autoawq" in str(exc_info.value)
236
+
237
+ def test_quantize_awq_quantization_failed(self, tmp_dir):
238
+ """测试 AWQ 量化过程失败的情况"""
239
+ model_path = os.path.join(tmp_dir, "model")
240
+ output_path = os.path.join(tmp_dir, "model-awq")
241
+
242
+ with patch("hos_optimizer.quantize.AutoAWQForCausalLM") as mock_awq_cls, \
243
+ patch("hos_optimizer.quantize.AutoTokenizer") as mock_tokenizer_cls, \
244
+ patch("hos_optimizer.quantize.optimize_for_low_vram") as mock_optimize:
245
+
246
+ mock_model = MagicMock()
247
+ mock_tokenizer = MagicMock()
248
+ mock_awq_cls.from_pretrained.return_value = mock_model
249
+ mock_tokenizer_cls.from_pretrained.return_value = mock_tokenizer
250
+ mock_optimize.return_value = {"zero_point": True, "q_group_size": 128, "w_bit": 4, "version": "GEMM"}
251
+
252
+ # 量化过程抛出异常
253
+ mock_model.quantize.side_effect = Exception("Quantization failed")
254
+
255
+ with pytest.raises(QuantizationError) as exc_info:
256
+ quantize_awq(model_path, output_path)
257
+
258
+ assert "AWQ 量化失败" in str(exc_info.value)
259
+
260
+
261
+ class TestGPTQQuantization:
262
+ """GPTQ 量化测试"""
263
+
264
+ def test_quantize_gptq_success(self, tmp_dir):
265
+ """测试 GPTQ 量化成功场景"""
266
+ model_path = os.path.join(tmp_dir, "model")
267
+ output_path = os.path.join(tmp_dir, "model-gptq")
268
+
269
+ with patch("hos_optimizer.quantize.AutoGPTQForCausalLM") as mock_gptq_cls, \
270
+ patch("hos_optimizer.quantize.AutoTokenizer") as mock_tokenizer_cls, \
271
+ patch("hos_optimizer.quantize.BaseQuantizeConfig") as mock_config_cls:
272
+
273
+ mock_model = MagicMock()
274
+ mock_tokenizer = MagicMock()
275
+ mock_config = MagicMock()
276
+
277
+ mock_gptq_cls.from_pretrained.return_value = mock_model
278
+ mock_tokenizer_cls.from_pretrained.return_value = mock_tokenizer
279
+ mock_config_cls.return_value = mock_config
280
+
281
+ # Mock tokenizer 调用
282
+ mock_tokenizer.return_value = {"input_ids": MagicMock()}
283
+
284
+ result = quantize_gptq(
285
+ model_path=model_path,
286
+ output_path=output_path,
287
+ bits=4,
288
+ group_size=128,
289
+ desc_act=False
290
+ )
291
+
292
+ assert result == output_path
293
+ mock_model.quantize.assert_called_once()
294
+ mock_model.save_quantized.assert_called_once_with(output_path)
295
+
296
+ def test_quantize_gptq_invalid_bits(self, tmp_dir):
297
+ """测试 GPTQ 量化使用无效位数的情况"""
298
+ model_path = os.path.join(tmp_dir, "model")
299
+ output_path = os.path.join(tmp_dir, "model-gptq")
300
+
301
+ with pytest.raises(ValueError) as exc_info:
302
+ quantize_gptq(model_path, output_path, bits=3)
303
+
304
+ assert "仅支持 4-bit 或 8-bit" in str(exc_info.value)
305
+
306
+ def test_quantize_gptq_missing_dependency(self, tmp_dir):
307
+ """测试 GPTQ 量化缺少依赖的情况"""
308
+ model_path = os.path.join(tmp_dir, "model")
309
+ output_path = os.path.join(tmp_dir, "model-gptq")
310
+
311
+ with patch("hos_optimizer.quantize.AutoGPTQForCausalLM") as mock_gptq_cls:
312
+ mock_gptq_cls.from_pretrained.side_effect = ImportError("auto_gptq")
313
+
314
+ with pytest.raises(QuantizationError) as exc_info:
315
+ quantize_gptq(model_path, output_path, bits=4)
316
+
317
+ assert "缺少依赖" in str(exc_info.value)
318
+ assert "auto-gptq" in str(exc_info.value)
319
+
320
+
321
+ class TestPerplexityEvaluation:
322
+ """PPL 评估测试"""
323
+
324
+ def test_evaluate_perplexity_success(self, tmp_dir):
325
+ """测试 PPL 评估成功场景"""
326
+ model_path = os.path.join(tmp_dir, "model")
327
+
328
+ with patch("hos_optimizer.quantize.AutoModelForCausalLM") as mock_model_cls, \
329
+ patch("hos_optimizer.quantize.AutoTokenizer") as mock_tokenizer_cls, \
330
+ patch("hos_optimizer.quantize.load_dataset") as mock_load_dataset:
331
+
332
+ mock_model = MagicMock()
333
+ mock_tokenizer = MagicMock()
334
+ mock_dataset = MagicMock()
335
+
336
+ mock_model_cls.from_pretrained.return_value = mock_model
337
+ mock_tokenizer_cls.from_pretrained.return_value = mock_tokenizer
338
+ mock_load_dataset.return_value = mock_dataset
339
+
340
+ # Mock 数据集
341
+ mock_dataset.__getitem__.return_value = ["text1", "text2"]
342
+
343
+ # Mock tokenizer 调用
344
+ mock_encodings = MagicMock()
345
+ mock_encodings.input_ids.size.return_value = (1, 100)
346
+ mock_tokenizer.return_value = mock_encodings
347
+
348
+ # Mock 模型推理
349
+ mock_model.return_value = MagicMock(loss=MagicMock(item=MagicMock(return_value=2.5)))
350
+ mock_model.device = "cpu"
351
+
352
+ result = evaluate_perplexity(
353
+ model_path=model_path,
354
+ dataset="wikitext",
355
+ max_samples=10,
356
+ stride=512
357
+ )
358
+
359
+ assert isinstance(result, float)
360
+ assert result > 0
361
+
362
+ def test_evaluate_perplexity_missing_dataset(self, tmp_dir):
363
+ """测试 PPL 评估缺少 datasets 库的情况"""
364
+ model_path = os.path.join(tmp_dir, "model")
365
+
366
+ with patch("hos_optimizer.quantize.AutoModelForCausalLM") as mock_model_cls, \
367
+ patch("hos_optimizer.quantize.AutoTokenizer") as mock_tokenizer_cls:
368
+
369
+ mock_model_cls.from_pretrained.return_value = MagicMock()
370
+ mock_tokenizer_cls.from_pretrained.return_value = MagicMock()
371
+
372
+ with patch("hos_optimizer.quantize.load_dataset", side_effect=ImportError("datasets")):
373
+ with pytest.raises(QuantizationError) as exc_info:
374
+ evaluate_perplexity(model_path)
375
+
376
+ assert "缺少依赖" in str(exc_info.value)
377
+ assert "datasets" in str(exc_info.value)
378
+
379
+
380
+ class TestFormatConversion:
381
+ """格式转换测试"""
382
+
383
+ def test_convert_hf_to_gguf(self, tmp_dir):
384
+ """测试 HuggingFace 到 GGUF 格式转换"""
385
+ model_path = os.path.join(tmp_dir, "model")
386
+ output_path = os.path.join(tmp_dir, "model.gguf")
387
+
388
+ with patch("hos_optimizer.quantize.quantize_gguf") as mock_quantize:
389
+ mock_quantize.return_value = output_path
390
+
391
+ result = convert_format(
392
+ model_path=model_path,
393
+ output_path=output_path,
394
+ from_format="hf",
395
+ to_format="gguf"
396
+ )
397
+
398
+ assert result == output_path
399
+ mock_quantize.assert_called_once()
400
+
401
+ def test_convert_hf_to_awq(self, tmp_dir):
402
+ """测试 HuggingFace 到 AWQ 格式转换"""
403
+ model_path = os.path.join(tmp_dir, "model")
404
+ output_path = os.path.join(tmp_dir, "model-awq")
405
+
406
+ with patch("hos_optimizer.quantize.quantize_awq") as mock_quantize:
407
+ mock_quantize.return_value = output_path
408
+
409
+ result = convert_format(
410
+ model_path=model_path,
411
+ output_path=output_path,
412
+ from_format="hf",
413
+ to_format="awq"
414
+ )
415
+
416
+ assert result == output_path
417
+ mock_quantize.assert_called_once()
418
+
419
+ def test_convert_unsupported_path(self, tmp_dir):
420
+ """测试不支持的转换路径"""
421
+ model_path = os.path.join(tmp_dir, "model")
422
+ output_path = os.path.join(tmp_dir, "model-out")
423
+
424
+ with pytest.raises(QuantizationError) as exc_info:
425
+ convert_format(
426
+ model_path=model_path,
427
+ output_path=output_path,
428
+ from_format="gguf",
429
+ to_format="awq"
430
+ )
431
+
432
+ assert "不支持的转换路径" in str(exc_info.value)
433
+
434
+ def test_convert_gguf_to_hf_not_implemented(self, tmp_dir):
435
+ """测试 GGUF 到 HuggingFace 转换未实现"""
436
+ model_path = os.path.join(tmp_dir, "model.gguf")
437
+ output_path = os.path.join(tmp_dir, "model")
438
+
439
+ with pytest.raises(QuantizationError) as exc_info:
440
+ convert_format(
441
+ model_path=model_path,
442
+ output_path=output_path,
443
+ from_format="gguf",
444
+ to_format="hf"
445
+ )
446
+
447
+ assert "尚未实现" in str(exc_info.value)
448
+
449
+
450
+ class TestModelSize:
451
+ """模型大小计算测试"""
452
+
453
+ def test_get_model_size_empty_dir(self, tmp_dir):
454
+ """测试空目录的模型大小"""
455
+ size = get_model_size(tmp_dir)
456
+ assert size == 0.0
457
+
458
+ def test_get_model_size_with_files(self, tmp_dir):
459
+ """测试包含模型文件的目录大小"""
460
+ # 创建测试文件
461
+ test_file = os.path.join(tmp_dir, "model.safetensors")
462
+ with open(test_file, "wb") as f:
463
+ f.write(b"0" * (1024 * 1024)) # 1MB
464
+
465
+ size = get_model_size(tmp_dir)
466
+ assert size > 0
467
+ assert size < 0.01 # 应该约等于 0.001GB
468
+
469
+ def test_get_model_size_nonexistent_dir(self):
470
+ """测试不存在的目录"""
471
+ with pytest.raises(Exception):
472
+ get_model_size("/nonexistent/path")
473
+
474
+
475
+ if __name__ == "__main__":
476
+ pytest.main([__file__, "-v"])