test_generate_models_preset.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import re
  2. import unittest
  3. from unittest.mock import patch
  4. from pathlib import Path
  5. from generate_models_preset import (
  6. ModelEntry,
  7. ModelMetadata,
  8. build_parser,
  9. build_preset_values,
  10. classify_model_family,
  11. make_alias,
  12. render_ini,
  13. )
  14. def make_entry(
  15. *,
  16. filename: str = "model.gguf",
  17. architecture: str | None = "llama",
  18. context_length: int | None = 32768,
  19. block_count: int | None = 40,
  20. expert_count: int | None = None,
  21. expert_used_count: int | None = None,
  22. mmproj: bool = False,
  23. ) -> ModelEntry:
  24. return ModelEntry(
  25. publisher="publisher",
  26. model_dir="model-dir",
  27. model_path=Path(r"C:\models") / filename,
  28. mmproj_path=(Path(r"C:\models") / "mmproj.gguf") if mmproj else None,
  29. quant_key="Q4_K_M",
  30. metadata=ModelMetadata(
  31. architecture=architecture,
  32. context_length=context_length,
  33. block_count=block_count,
  34. expert_count=expert_count,
  35. expert_used_count=expert_used_count,
  36. size_label=None,
  37. general_name=None,
  38. file_size_gib=24.0,
  39. ),
  40. )
  41. class ClassificationTests(unittest.TestCase):
  42. def test_moe_detection_uses_metadata_not_filename(self):
  43. entry = make_entry(filename="mixtral-8x7b.gguf", architecture="llama")
  44. self.assertEqual(classify_model_family(entry), "dense")
  45. def test_moe_detection_accepts_moe_architecture_metadata(self):
  46. entry = make_entry(architecture="qwen35moe")
  47. self.assertEqual(classify_model_family(entry), "moe")
  48. def test_moe_metadata_takes_priority_over_mmproj(self):
  49. entry = make_entry(
  50. filename="plain-model.gguf",
  51. architecture="qwen35moe",
  52. expert_count=128,
  53. expert_used_count=8,
  54. mmproj=True,
  55. )
  56. self.assertEqual(classify_model_family(entry), "moe")
  57. class PresetValueTests(unittest.TestCase):
  58. def test_dense_values_keep_gpu_layers_field(self):
  59. entry = make_entry(
  60. filename="plain-model.gguf",
  61. architecture="llama",
  62. context_length=32768,
  63. block_count=40,
  64. )
  65. with patch("generate_models_preset.os.cpu_count", return_value=20):
  66. values = build_preset_values(entry, "smart")
  67. self.assertEqual(values["ctx-size"], "32768")
  68. self.assertEqual(values["n-gpu-layers"], "40")
  69. self.assertEqual(values["parallel"], "4")
  70. self.assertEqual(values["threads"], "14")
  71. def test_moe_values_include_broader_runtime_fields(self):
  72. entry = make_entry(
  73. filename="plain-model.gguf",
  74. architecture="qwen35moe",
  75. context_length=131072,
  76. block_count=99,
  77. expert_count=128,
  78. expert_used_count=8,
  79. )
  80. with patch("generate_models_preset.os.cpu_count", return_value=20):
  81. values = build_preset_values(entry, "smart")
  82. self.assertEqual(values["ctx-size"], "43008")
  83. self.assertEqual(values["n-gpu-layers"], "99")
  84. self.assertEqual(values["parallel"], "4")
  85. self.assertEqual(values["batch-size"], "2048")
  86. self.assertEqual(values["ubatch-size"], "512")
  87. self.assertEqual(values["n-cpu-moe"], "0")
  88. self.assertEqual(values["cache-type-k"], "q8_0")
  89. self.assertEqual(values["cache-type-v"], "q8_0")
  90. self.assertEqual(values["mmap"], "true")
  91. self.assertEqual(values["threads"], "14")
  92. self.assertEqual(values["temp"], "0.7")
  93. self.assertEqual(values["top-k"], "40")
  94. self.assertEqual(values["top-p"], "0.95")
  95. self.assertEqual(values["min-p"], "0.0")
  96. self.assertEqual(values["repeat-penalty"], "1.0")
  97. def test_moe_with_mmproj_still_gets_n_cpu_moe(self):
  98. entry = make_entry(
  99. filename="plain-model.gguf",
  100. architecture="qwen35moe",
  101. context_length=131072,
  102. block_count=99,
  103. expert_count=128,
  104. expert_used_count=8,
  105. mmproj=True,
  106. )
  107. with patch("generate_models_preset.os.cpu_count", return_value=20):
  108. values = build_preset_values(entry, "smart")
  109. self.assertEqual(values["n-cpu-moe"], "0")
  110. self.assertEqual(values["batch-size"], "2048")
  111. def test_dense_models_get_sampling_defaults(self):
  112. entry = make_entry(
  113. filename="plain-model.gguf",
  114. architecture="llama",
  115. context_length=32768,
  116. block_count=40,
  117. )
  118. with patch("generate_models_preset.os.cpu_count", return_value=20):
  119. values = build_preset_values(entry, "smart")
  120. self.assertEqual(values["temp"], "0.8")
  121. self.assertEqual(values["top-k"], "40")
  122. self.assertEqual(values["top-p"], "0.95")
  123. self.assertEqual(values["min-p"], "0.05")
  124. self.assertEqual(values["repeat-penalty"], "1.0")
  125. class RenderIniTests(unittest.TestCase):
  126. def test_render_ini_emits_range_comments_next_to_numeric_values(self):
  127. entry = make_entry(
  128. filename="plain-model.gguf",
  129. architecture="qwen35moe",
  130. context_length=131072,
  131. block_count=99,
  132. expert_count=128,
  133. expert_used_count=8,
  134. )
  135. with patch("generate_models_preset.os.cpu_count", return_value=20):
  136. text = render_ini(
  137. entries=[entry],
  138. models_dir=Path(r"C:\models-root"),
  139. select_mode="all",
  140. profile="smart",
  141. alias_style="section",
  142. backup_path=None,
  143. )
  144. self.assertRegex(
  145. text,
  146. re.compile(
  147. r"; ctx-size range = 512\.\.131072; chosen = 43008\s+ctx-size = 43008",
  148. re.MULTILINE,
  149. ),
  150. )
  151. self.assertRegex(
  152. text,
  153. re.compile(
  154. r"; n-gpu-layers range = 0\.\.99; chosen = 99\s+n-gpu-layers = 99",
  155. re.MULTILINE,
  156. ),
  157. )
  158. self.assertRegex(
  159. text,
  160. re.compile(
  161. r"; n-cpu-moe range = 0\.\.99; chosen = 0\s+n-cpu-moe = 0",
  162. re.MULTILINE,
  163. ),
  164. )
  165. self.assertRegex(
  166. text,
  167. re.compile(
  168. r"; threads range = 1\.\.20; chosen = 14\s+threads = 14",
  169. re.MULTILINE,
  170. ),
  171. )
  172. self.assertRegex(
  173. text,
  174. re.compile(
  175. r"; parallel range = 1\.\.4; chosen = 4\s+parallel = 4",
  176. re.MULTILINE,
  177. ),
  178. )
  179. self.assertRegex(
  180. text,
  181. re.compile(
  182. r"temp = 0\.7\s+top-k = 40\s+top-p = 0\.95\s+min-p = 0\.0\s+repeat-penalty = 1\.0",
  183. re.MULTILINE,
  184. ),
  185. )
  186. self.assertIn(
  187. "; Heuristic sampling defaults / 经验采样默认值, not GGUF metadata-derived",
  188. text,
  189. )
  190. class ThreadHeuristicsTests(unittest.TestCase):
  191. def test_threads_use_seventy_percent_of_logical_cpu_count(self):
  192. entry = make_entry()
  193. with patch("generate_models_preset.os.cpu_count", return_value=20):
  194. values = build_preset_values(entry, "smart")
  195. self.assertEqual(values["threads"], "14")
  196. class CliTests(unittest.TestCase):
  197. def test_backup_is_disabled_by_default(self):
  198. parser = build_parser()
  199. args = parser.parse_args(["--models-dir", r"C:\models-root"])
  200. self.assertFalse(args.backup)
  201. class AliasTests(unittest.TestCase):
  202. def test_section_alias_matches_router_model_identifier_shape(self):
  203. alias = make_alias(
  204. publisher="ggml-org",
  205. model_dir="Qwen3-8B-GGUF",
  206. model_file="Qwen3-8B-Q4_K_M.gguf",
  207. alias_style="section",
  208. used_aliases=set(),
  209. )
  210. self.assertEqual(alias, "ggml-org/Qwen3-8B-GGUF:Q4_K_M")
  211. if __name__ == "__main__":
  212. unittest.main()