| """ |
| 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_REPO = "nm-testing/tiny-testing-random-weights" |
| SMALL_MODEL_REPO = "nm-testing/tinysmokellama-3.2" |
|
|
| |
|
|
|
|
| @pytest.mark.smoke |
| def test_is_config_only_dir(tmp_path): |
| |
| assert is_config_only_dir(tmp_path / "does-not-exist") is False |
| assert is_config_only_dir(tmp_path) is False |
|
|
| |
| (tmp_path / "config.json").write_text("{}") |
| assert is_config_only_dir(tmp_path) is True |
|
|
| |
| (tmp_path / "model.safetensors").write_text("") |
| assert is_config_only_dir(tmp_path) is False |
|
|
|
|
| @pytest.mark.smoke |
| 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 |
|
|
|
|
| @pytest.mark.smoke |
| @pytest.mark.parametrize( |
| "index_file", |
| ["model.safetensors.index.json", "pytorch_model.bin.index.json"], |
| ) |
| def test_is_config_only_dir_detects_sharded_index(tmp_path, index_file): |
| |
| |
| (tmp_path / "config.json").write_text("{}") |
| (tmp_path / index_file).write_text("{}") |
|
|
| assert is_config_only_dir(tmp_path) is False |
|
|
|
|
| |
|
|
|
|
| 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", |
| } |
|
|
|
|
| @pytest.mark.smoke |
| def test_resolve_key_exact_match(): |
| assert _resolve_key("model.embed_tokens.weight", FAKE_WEIGHT_MAP) == ( |
| "model.embed_tokens.weight" |
| ) |
|
|
|
|
| @pytest.mark.smoke |
| 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" |
| ) |
|
|
|
|
| @pytest.mark.smoke |
| def test_resolve_key_alias_exact(): |
| wm = {"tok_embeddings.weight": "shard.safetensors"} |
| assert _resolve_key("embed_tokens.weight", wm) == "tok_embeddings.weight" |
|
|
|
|
| @pytest.mark.smoke |
| def test_resolve_key_alias_suffix(): |
| wm = {"model.tok_embeddings.weight": "shard.safetensors"} |
| assert _resolve_key("embed_tokens.weight", wm) == "model.tok_embeddings.weight" |
|
|
|
|
| @pytest.mark.smoke |
| 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" |
|
|
|
|
| @pytest.mark.smoke |
| def test_resolve_key_miss(): |
| assert _resolve_key("nonexistent.weight", FAKE_WEIGHT_MAP) is None |
|
|
|
|
| @pytest.mark.smoke |
| 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" |
|
|
|
|
| @pytest.mark.smoke |
| 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", |
| } |
| |
| |
| |
| assert _resolve_key("model.norm.weight", wm) == "model.text.norm.weight" |
|
|
|
|
| @pytest.mark.smoke |
| 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" |
|
|
|
|
| |
|
|
|
|
| @pytest.mark.sanity |
| 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" |
|
|
|
|
| |
|
|
|
|
| @pytest.mark.sanity |
| @pytest.mark.parametrize( |
| "test_model_repo", |
| [ |
| TEST_MODEL_REPO, |
| SMALL_MODEL_REPO, |
| ], |
| ) |
| 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) |
| |
| assert ( |
| result["model.embed_tokens.weight"].shape[0] |
| == result["lm_head.weight"].shape[0] |
| ) |
| |
| assert result["model.embed_tokens.weight"].device.type == "cpu" |
|
|
|
|
| @pytest.mark.sanity |
| def test_load_model_layers_matches_full_model(): |
| """Test that tensors loaded via utility match those from full model loading.""" |
| |
| full_model = AutoModelForCausalLM.from_pretrained( |
| TEST_MODEL_REPO, |
| torch_dtype="auto", |
| ) |
|
|
| |
| state_dict = full_model.state_dict() |
|
|
| |
| 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) |
|
|
| |
| 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] |
|
|
| |
| assert util_tensor.dtype == model_tensor.dtype, ( |
| f"Dtype mismatch for {layer_name}: " |
| f"{util_tensor.dtype} vs {model_tensor.dtype}" |
| ) |
|
|
| |
| assert util_tensor.shape == model_tensor.shape, ( |
| f"Shape mismatch for {layer_name}: " |
| f"{util_tensor.shape} vs {model_tensor.shape}" |
| ) |
|
|
| |
| assert torch.equal(util_tensor, model_tensor), ( |
| f"Tensor values don't match for {layer_name}" |
| ) |
|
|