Download source/tests/unit/utils/test_loading.py from khazic/spec-b300: direct link, hf CLI and curl.
- Browser
- Download file 7.74 kB
-
https://huggingface.co/khazic/spec-b300/resolve/main/source/tests/unit/utils/test_loading.py
- Command line
-
hf download hf://khazic/spec-b300/source/tests/unit/utils/test_loading.py
-
curl -L -o test_loading.py https://huggingface.co/khazic/spec-b300/resolve/main/source/tests/unit/utils/test_loading.py
7.74 kB
| """ | |
| Unit tests for the loading module in the Speculators library. | |
| """ | |
| import pytest | |
| import torch | |
| from transformers import AutoModelForCausalLM | |
| from speculators.utils.loading import ( | |
| _resolve_file, | |
| _resolve_key, | |
| is_config_only_dir, | |
| load_model_layers, | |
| ) | |
| # Test model from HuggingFace | |
| TEST_MODEL_REPO = "nm-testing/tiny-testing-random-weights" | |
| SMALL_MODEL_REPO = "nm-testing/tinysmokellama-3.2" | |
| # is_config_only_dir Tests | |
| def test_is_config_only_dir(tmp_path): | |
| # Missing directory and a directory without config.json are not config-only. | |
| assert is_config_only_dir(tmp_path / "does-not-exist") is False | |
| assert is_config_only_dir(tmp_path) is False | |
| # config.json present, no weights -> config-only. | |
| (tmp_path / "config.json").write_text("{}") | |
| assert is_config_only_dir(tmp_path) is True | |
| # A weight file makes it a full checkpoint. | |
| (tmp_path / "model.safetensors").write_text("") | |
| assert is_config_only_dir(tmp_path) is False | |
| def test_is_config_only_dir_detects_bin_weights(tmp_path): | |
| (tmp_path / "config.json").write_text("{}") | |
| (tmp_path / "pytorch_model.bin").write_text("") | |
| assert is_config_only_dir(tmp_path) is False | |
| def test_is_config_only_dir_detects_sharded_index(tmp_path, index_file): | |
| # A sharded-checkpoint manifest ends in .json (so it dodges the *.safetensors / | |
| # *.bin globs); it must still count as weights, not config-only. | |
| (tmp_path / "config.json").write_text("{}") | |
| (tmp_path / index_file).write_text("{}") | |
| assert is_config_only_dir(tmp_path) is False | |
| # _resolve_key Tests | |
| FAKE_WEIGHT_MAP = { | |
| "model.embed_tokens.weight": "shard-0.safetensors", | |
| "model.layers.0.self_attn.q_proj.weight": "shard-0.safetensors", | |
| "tok_embeddings.weight": "shard-1.safetensors", | |
| "output.weight": "shard-1.safetensors", | |
| "norm.weight": "shard-1.safetensors", | |
| } | |
| def test_resolve_key_exact_match(): | |
| assert _resolve_key("model.embed_tokens.weight", FAKE_WEIGHT_MAP) == ( | |
| "model.embed_tokens.weight" | |
| ) | |
| def test_resolve_key_suffix_match(): | |
| assert _resolve_key("self_attn.q_proj.weight", FAKE_WEIGHT_MAP) == ( | |
| "model.layers.0.self_attn.q_proj.weight" | |
| ) | |
| def test_resolve_key_alias_exact(): | |
| wm = {"tok_embeddings.weight": "shard.safetensors"} | |
| assert _resolve_key("embed_tokens.weight", wm) == "tok_embeddings.weight" | |
| def test_resolve_key_alias_suffix(): | |
| wm = {"model.tok_embeddings.weight": "shard.safetensors"} | |
| assert _resolve_key("embed_tokens.weight", wm) == "model.tok_embeddings.weight" | |
| def test_resolve_key_all_aliases(): | |
| wm_lm = {"output.weight": "s.safetensors"} | |
| assert _resolve_key("lm_head.weight", wm_lm) == "output.weight" | |
| wm_norm = {"norm.weight": "s.safetensors"} | |
| assert _resolve_key("model.norm.weight", wm_norm) == "norm.weight" | |
| def test_resolve_key_miss(): | |
| assert _resolve_key("nonexistent.weight", FAKE_WEIGHT_MAP) is None | |
| def test_resolve_key_prefers_exact_over_alias(): | |
| wm = { | |
| "embed_tokens.weight": "shard-0.safetensors", | |
| "tok_embeddings.weight": "shard-1.safetensors", | |
| } | |
| assert _resolve_key("embed_tokens.weight", wm) == "embed_tokens.weight" | |
| def test_resolve_key_prefers_shortest_suffix(): | |
| """When several keys share the searched suffix, the shortest (most specific) | |
| one wins via ``min(matches, key=len)``. | |
| None of the keys match an alias here, so resolution falls through to the | |
| generic ``norm.weight`` suffix scan and the tie-break is exercised directly | |
| rather than short-circuited by an alias hit (see ``test_resolve_key_llm_aliases`` | |
| for the alias path). | |
| """ | |
| wm = { | |
| "model.audio.final_norm.weight": "shard-a.safetensors", | |
| "model.text.norm.weight": "shard-b.safetensors", | |
| } | |
| # Both keys end in "norm.weight" (reached via the model.norm.weight -> | |
| # norm.weight alias); the shorter, more-specific key must win over the audio | |
| # tower's norm. | |
| assert _resolve_key("model.norm.weight", wm) == "model.text.norm.weight" | |
| def test_resolve_key_llm_aliases(): | |
| """Inkling-style keys with llm. prefix resolve correctly.""" | |
| wm = { | |
| "model.llm.embed.weight": "shard-0.safetensors", | |
| "model.llm.unembed.weight": "shard-1.safetensors", | |
| "model.llm.norm.weight": "shard-2.safetensors", | |
| } | |
| assert _resolve_key("embed_tokens.weight", wm) == "model.llm.embed.weight" | |
| assert _resolve_key("lm_head.weight", wm) == "model.llm.unembed.weight" | |
| assert _resolve_key("model.norm.weight", wm) == "model.llm.norm.weight" | |
| # _resolve_file Tests | |
| def test_resolve_file_hub_download(): | |
| """Test resolving a file from HuggingFace Hub using real model.""" | |
| result = _resolve_file(TEST_MODEL_REPO, "config.json") | |
| assert result.exists() | |
| assert result.name == "config.json" | |
| # load_model_layers Tests | |
| def test_load_model(test_model_repo: str): | |
| """Test loading layers from a model repository.""" | |
| result = load_model_layers( | |
| ["model.embed_tokens.weight", "lm_head.weight"], | |
| test_model_repo, | |
| ) | |
| assert len(result) == 2 | |
| assert "model.embed_tokens.weight" in result | |
| assert "lm_head.weight" in result | |
| assert isinstance(result["model.embed_tokens.weight"], torch.Tensor) | |
| assert isinstance(result["lm_head.weight"], torch.Tensor) | |
| # Both should have same vocab dimension | |
| assert ( | |
| result["model.embed_tokens.weight"].shape[0] | |
| == result["lm_head.weight"].shape[0] | |
| ) | |
| # Verify CPU device | |
| assert result["model.embed_tokens.weight"].device.type == "cpu" | |
| def test_load_model_layers_matches_full_model(): | |
| """Test that tensors loaded via utility match those from full model loading.""" | |
| # Load full model | |
| full_model = AutoModelForCausalLM.from_pretrained( | |
| TEST_MODEL_REPO, | |
| torch_dtype="auto", | |
| ) | |
| # Get state dict from full model | |
| state_dict = full_model.state_dict() | |
| # Load specific layers using our utility | |
| layer_names = [ | |
| "model.embed_tokens.weight", | |
| "lm_head.weight", | |
| "model.norm.weight", | |
| "model.layers.0.input_layernorm.weight", | |
| "model.layers.0.mlp.gate_proj.weight", | |
| "model.layers.1.mlp.down_proj.weight", | |
| ] | |
| loaded_tensors = load_model_layers(layer_names, TEST_MODEL_REPO) | |
| # Compare each tensor | |
| for layer_name in layer_names: | |
| assert layer_name in loaded_tensors, f"Layer {layer_name} not loaded" | |
| assert layer_name in state_dict, f"Layer {layer_name} not in state_dict" | |
| util_tensor = loaded_tensors[layer_name] | |
| model_tensor = state_dict[layer_name] | |
| # Check dtype matches | |
| assert util_tensor.dtype == model_tensor.dtype, ( | |
| f"Dtype mismatch for {layer_name}: " | |
| f"{util_tensor.dtype} vs {model_tensor.dtype}" | |
| ) | |
| # Check shape matches | |
| assert util_tensor.shape == model_tensor.shape, ( | |
| f"Shape mismatch for {layer_name}: " | |
| f"{util_tensor.shape} vs {model_tensor.shape}" | |
| ) | |
| # Check values are identical | |
| assert torch.equal(util_tensor, model_tensor), ( | |
| f"Tensor values don't match for {layer_name}" | |
| ) | |