Files

166 lines
7.9 KiB
Python

import importlib
import sys
import types
from dataclasses import FrozenInstanceError
from pathlib import Path
import pytest
@pytest.fixture(scope="module")
def rules_module():
name = "modelutils_layer_rule_tests"
package = types.ModuleType(name)
package.__path__ = [str(Path(__file__).resolve().parents[1])]
sys.modules[name] = package
return importlib.import_module(f"{name}.nodes.layer_parameters")
def resolve(module, text, *, profile="extract:fixed", names=("blocks.45.attn.qkv_proj.weight",), defaults=None, glob=False):
return module.resolve_layer_parameters(
module.parse_rules(text, glob), profile, names,
defaults or {"linear_dim": 8, "conv_dim": 4, "clamp_quantile": 0.99, "min_diff": 0},
node_name="Example Node",
)
@pytest.mark.parametrize("assignments", [
"a:64 b:32 c:0.99 d:0.0",
"a : 64, b:32 c:0.99, d : 0.0",
"linear_dim:64 b:3.2e1 clamp_quantile:+.99 d:0e0",
])
def test_aliases_separators_and_inheritance(rules_module, assignments):
module = rules_module
result = resolve(module, rf"(blocks\.4[589]\.attn\.qkv_proj) {assignments}")
assert result == {"blocks.45.attn.qkv_proj.weight": {
"linear_dim": 64, "conv_dim": 32, "clamp_quantile": 0.99, "min_diff": 0,
}}
partial = resolve(module, "(qkv_proj) a:64", names=("other", "blocks.45.attn.qkv_proj.weight"))
assert "other" not in partial
assert partial["blocks.45.attn.qkv_proj.weight"]["conv_dim"] == 4
def test_patterns_preserve_groups_spaces_escapes_and_lines(rules_module):
text = " # explanation\r\n\r\n" + r"((?:blocks\.45|blocks\.49)\.my layer\.(?:qkv_proj)) a:16"
parsed = rules_module.parse_rules(text)
assert parsed.rules[0].line == 3
assert parsed.rules[0].pattern == r"(?:blocks\.45|blocks\.49)\.my layer\.(?:qkv_proj)"
result = rules_module.resolve_layer_parameters(parsed, "extract:fixed", ["blocks.49.my layer.qkv_proj"], {}, node_name="Fixed")
assert result["blocks.49.my layer.qkv_proj"]["linear_dim"] == 16
@pytest.mark.parametrize("text", [
"() a:1", "( ) a:1", "(layer)", "layer a:1", "(layer)a:1",
"([invalid) a:1", "(layer) a:", "(layer) a:nan", "(layer) a:inf",
"(layer) a:1e309", "(layer) a:1a:2", "(layer) a:1,", "(layer) a:1,,b:2",
"(layer) a:1 # comment", "(layer) a:1 a:2", "(layer) a:1 rubbish",
])
def test_invalid_syntax_errors_at_configurator(rules_module, text):
with pytest.raises(ValueError, match="line 1"):
rules_module.LayerParameterConfiguration.execute(text)
@pytest.mark.parametrize("assignments, message", [
("a:1 alpha:2", "unsupported"),
("a:1 linear_dim:1", "duplicate aliases"),
("a:1.5", "integral"),
("a:0", "linear_dim"),
("a:16385", "linear_dim"),
("c:0.49", "clamp_quantile"),
("d:1.01", "min_diff"),
("seed:1", "unsupported"),
])
def test_receiver_validates_parameter_domains(rules_module, assignments, message):
with pytest.raises(ValueError, match=message) as error:
resolve(rules_module, f"(qkv_proj) {assignments}")
assert "Example Node" in str(error.value)
assert "line 1" in str(error.value)
def test_strict_overlap_even_for_disjoint_assignments(rules_module):
with pytest.raises(ValueError, match="conflicting") as error:
resolve(rules_module, "(blocks) a:32\n\n(qkv_proj) b:16")
assert "blocks.45.attn.qkv_proj.weight" in str(error.value)
assert "line 1" in str(error.value) and "line 3" in str(error.value)
def test_zero_match_and_repeated_logical_names(rules_module):
with pytest.raises(ValueError, match="line 2"):
resolve(rules_module, "(blocks) a:32\n(missing) b:16")
assert resolve(rules_module, "(qkv_proj) a:32", names=["qkv_proj", "qkv_proj"]) == {
"qkv_proj": {"linear_dim": 32, "conv_dim": 4, "clamp_quantile": 0.99, "min_diff": 0},
}
def test_glob_is_case_sensitive_substring(rules_module):
result = resolve(rules_module, "(blocks.4[589].attn.qkv_*) a:32", glob=True)
assert len(result) == 1
with pytest.raises(ValueError, match="no target layer"):
resolve(rules_module, "(BLOCKS*) a:32", glob=True)
def test_frobenius_validates_inherited_bounds(rules_module):
defaults = {"max_rank": 128, "min_rank": 32, "target": 0.9}
with pytest.raises(ValueError, match="min_rank=32 exceeds max_rank=16"):
resolve(rules_module, "(qkv_proj) a:16", profile="resize:frobenius", defaults=defaults)
result = resolve(rules_module, "(qkv_proj) a:16 b:1", profile="resize:frobenius", defaults=defaults)
assert next(iter(result.values())) == {"max_rank": 16, "min_rank": 1, "target": 0.9}
assert defaults["max_rank"] == 128
def test_shared_rules_do_not_leak_receiver_state(rules_module):
rules = rules_module.parse_rules("(layer) a:2")
with pytest.raises(FrozenInstanceError):
rules.glob_patterns = True
first = rules_module.resolve_layer_parameters(rules, "extract:fixed", ["layer"], {"conv_dim": 4}, node_name="A")
second = rules_module.resolve_layer_parameters(rules, "resize:fixed", ["layer"], {"new_rank": 8}, node_name="B")
first["layer"]["linear_dim"] = 64
assert second == {"layer": {"new_rank": 2}}
assert rules.rules[0].assignments == (("a", 2),)
assert rules_module.resolve_layer_parameters(None, "anything", [], {}, node_name="A") == {}
assert rules_module.resolve_layer_parameters(rules_module.parse_rules("\n#empty"), "anything", [], {}, node_name="A") == {}
with pytest.raises(TypeError, match="Layer Parameter Configuration"):
rules_module.resolve_layer_parameters("not a config", "extract:fixed", [], {}, node_name="A")
def test_fixed_dimensions_update_only_explicit_rank_caps(rules_module):
defaults = {"linear_dim": 8, "conv_dim": 4, "linear_max_rank": 2, "conv_max_rank": 1, "clamp_quantile": 0.99, "min_diff": 0}
result = resolve(rules_module, "(qkv_proj) a:8", defaults=defaults)
values = next(iter(result.values()))
assert values["linear_max_rank"] == 8
assert values["conv_max_rank"] == 1
values = next(iter(resolve(rules_module, "(qkv_proj) c:.9", defaults=defaults).values()))
assert values["linear_max_rank"] == 2
assert values["conv_max_rank"] == 1
def test_merge_aliases_mode_restrictions_and_quantiles(rules_module):
result = resolve(rules_module, "(qkv_proj) a:.5 b:.25 g:.75 d:1", profile="merge:Extract-Features", defaults={})
assert next(iter(result.values()))["gamma"] == 0.75
with pytest.raises(ValueError, match="duplicate aliases"):
resolve(rules_module, "(qkv_proj) c:.5 g:.5", profile="merge:Extract-Features")
with pytest.raises(ValueError, match="unsupported parameter"):
resolve(rules_module, "(qkv_proj) b:.5", profile="merge:Weight-Sum")
with pytest.raises(ValueError, match="unsupported parameter"):
resolve(rules_module, "(qkv_proj) a:.5", profile="merge:Power-Up Enhanced (DARE+TIES)")
with pytest.raises(ValueError, match="must be in"):
resolve(rules_module, "(qkv_proj) b:1.1", profile="merge:Power-Up (DARE+TIES)")
def test_configurator_documentation_and_socket_types(rules_module):
module = rules_module
schema = module.LayerParameterConfiguration.define_schema()
assert [item.id for item in schema.inputs] == ["rules", "glob_patterns"]
config, docs = module.LayerParameterConfiguration.execute("(layer) a:32")
assert isinstance(config, module.LayerParameterRules)
assert docs.startswith("# Per-layer parameter rules")
assert len(schema.outputs) == 2
profiles = [*(f"merge:{mode}" for mode in module.MERGE_MODES), *(f"extract:{mode}" for mode in module.EXTRACTION_BINDINGS), *(f"resize:{mode}" for mode in module.RESIZE_BINDINGS)]
for profile in profiles:
assert profile in docs
widget = module.parameter_input("merge" if profile.startswith("merge:") else profile)
assert widget.optional
for parameter in module.parameter_bindings(profile):
assert parameter.name in widget.tooltip
assert parameter.name in docs