Files
OBLITERATUS/tests/test_runtime_contracts.py
T

502 lines
16 KiB
Python

"""Mutation-friendly contracts for model, architecture, and device decisions."""
from __future__ import annotations
import pytest
from hypothesis import given, strategies as st
from obliteratus.runtime_contracts import (
ModelLoadPolicy,
attention_projection_names,
classify_weight_storage,
classify_architecture_size,
effective_model_memory_gb,
is_quantized_parameter,
norm_restoration_ratio,
quantized_model_fits_gpu,
resolve_model_load_policy,
should_snapshot_model,
supports_bfloat16_target,
validate_model_load_request,
)
@pytest.mark.parametrize(
("original", "current", "expected"),
[
(10.0, 10.0, None),
(0.0, 4.0, None),
(10.0, 0.0, None),
(float("nan"), 4.0, None),
(10.0, float("inf"), None),
(1.0, 0.5, 1.1),
(8.0, 10.0, 0.8),
(12.0, 10.0, 1.1),
],
)
def test_norm_restoration_ratio_is_bounded_and_fail_closed(original, current, expected):
assert norm_restoration_ratio(original, current, max_ratio=1.1) == expected
def test_norm_restoration_ratio_rejects_invalid_policy_bounds():
with pytest.raises(ValueError) as error:
norm_restoration_ratio(2.0, 1.0, max_ratio=0.0)
assert str(error.value) == "max norm ratio must be a positive finite number"
def test_norm_restoration_ratio_allows_subunit_policy_cap():
assert norm_restoration_ratio(2.0, 1.0, max_ratio=0.5) == 0.5
def test_norm_restoration_ratio_tolerance_boundary_is_a_noop():
assert norm_restoration_ratio(
1.0,
1.25,
max_ratio=1.1,
tolerance=0.25,
) is None
@pytest.mark.parametrize("model_name", [None, 7, "", " \t"])
def test_model_name_must_be_a_nonempty_string(model_name):
with pytest.raises(ValueError) as error:
validate_model_load_request(model_name, "causal_lm", None, "float32")
assert str(error.value) == "model_name must be a non-empty HuggingFace identifier or local path"
@pytest.mark.parametrize(
("field", "value", "message"),
[
("task", "embedding", "Unknown task 'embedding'. Choose from ['causal_lm', 'classification']"),
("quantization", "3bit", "Unknown quantization '3bit'. Choose None, '4bit', or '8bit'"),
("dtype", "float128", "Unknown dtype 'float128'. Choose from ['float32', 'float16', 'bfloat16']"),
],
)
def test_invalid_loader_enums_are_rejected(field, value, message):
request = {
"model_name": "local/model",
"task": "causal_lm",
"quantization": None,
"dtype": "float32",
}
request[field] = value
with pytest.raises(ValueError) as error:
validate_model_load_request(**request)
assert str(error.value) == message
@pytest.mark.parametrize("task", ["causal_lm", "classification"])
@pytest.mark.parametrize("quantization", [None, "4bit", "8bit"])
@pytest.mark.parametrize("dtype", ["float32", "float16", "bfloat16"])
def test_every_documented_loader_enum_combination_is_valid(task, quantization, dtype):
validate_model_load_request("local/model", task, quantization, dtype)
def test_loader_task_validation_can_follow_the_provider_registry():
validate_model_load_request(
"local/model",
"custom_task",
None,
"float32",
valid_tasks={"custom_task": object()},
)
@pytest.mark.parametrize(
("overrides", "expected"),
[
(
{},
ModelLoadPolicy("none", True, False, True),
),
(
{"device": "auto", "device_map_auto_supported": True},
ModelLoadPolicy("none", True, True, False),
),
(
{"device": "auto", "resolved_device": "mps"},
ModelLoadPolicy("none", True, False, True),
),
(
{"quantization": "4bit", "bitsandbytes_supported": True},
ModelLoadPolicy("bitsandbytes", True, True, False),
),
(
{"quantization": "8bit", "bitsandbytes_supported": True},
ModelLoadPolicy("bitsandbytes", True, True, False),
),
(
{"quantization": "4bit", "has_native_quantization": True},
ModelLoadPolicy("native", False, True, False),
),
],
)
def test_model_load_policy_precedence_is_explicit(overrides, expected):
inputs = {
"device": "cpu",
"resolved_device": "cpu",
"dtype": "float16",
"quantization": None,
"has_native_quantization": False,
"device_map_auto_supported": False,
"bitsandbytes_supported": False,
}
inputs.update(overrides)
assert resolve_model_load_policy(**inputs) == expected
def test_model_load_policy_rejects_unsupported_quantization_before_branching():
with pytest.raises(ValueError) as error:
resolve_model_load_policy(
device="auto",
resolved_device="cuda",
dtype="float16",
quantization="3bit",
has_native_quantization=False,
device_map_auto_supported=True,
bitsandbytes_supported=True,
)
assert str(error.value) == "Unknown quantization '3bit'. Choose None, '4bit', or '8bit'"
@pytest.mark.parametrize("quantization", ["4bit", "8bit"])
def test_requested_quantization_requires_a_compatible_resolved_device(quantization):
with pytest.raises(RuntimeError) as error:
resolve_model_load_policy(
device="auto",
resolved_device="mps",
dtype="bfloat16",
quantization=quantization,
has_native_quantization=False,
device_map_auto_supported=False,
bitsandbytes_supported=False,
)
assert str(error.value) == (
f"Quantization '{quantization}' requires an available NVIDIA CUDA device; "
"resolved device was 'mps'. Remove --quantization to load in bfloat16."
)
_OUTPUT_NAMES = ("o_proj",)
_INPUT_NAMES = ("q_proj", "k_proj", "v_proj", "k_norm")
@pytest.mark.parametrize("layer_index", range(6))
def test_nonshared_attention_projects_every_configured_weight(layer_index):
assert attention_projection_names(
projection_target="all",
layer_index=layer_index,
num_layers=6,
num_kv_shared_layers=0,
output_names=_OUTPUT_NAMES,
input_names=_INPUT_NAMES,
) == _OUTPUT_NAMES + _INPUT_NAMES
def test_output_only_projection_is_independent_of_shared_kv_layout():
assert attention_projection_names(
projection_target="output",
layer_index=5,
num_layers=6,
num_kv_shared_layers=3,
output_names=_OUTPUT_NAMES,
input_names=_INPUT_NAMES,
) == _OUTPUT_NAMES
def test_nonshared_projection_does_not_require_shared_storage_ownership_metadata():
assert attention_projection_names(
projection_target="all",
layer_index=-1,
num_layers=0,
num_kv_shared_layers=0,
output_names=_OUTPUT_NAMES,
input_names=_INPUT_NAMES,
) == _OUTPUT_NAMES + _INPUT_NAMES
@pytest.mark.parametrize(
("layer_index", "expected"),
[
(2, _OUTPUT_NAMES + _INPUT_NAMES),
(3, _OUTPUT_NAMES + _INPUT_NAMES),
(4, ("o_proj", "q_proj")),
(5, ("o_proj", "q_proj")),
],
)
def test_shared_kv_owner_projects_storage_once_and_borrowers_skip_it(layer_index, expected):
assert attention_projection_names(
projection_target="all",
layer_index=layer_index,
num_layers=6,
num_kv_shared_layers=3,
output_names=_OUTPUT_NAMES,
input_names=_INPUT_NAMES,
) == expected
def test_single_layer_can_own_kv_storage_shared_by_the_whole_model():
assert attention_projection_names(
projection_target="all",
layer_index=0,
num_layers=1,
num_kv_shared_layers=1,
output_names=_OUTPUT_NAMES,
input_names=_INPUT_NAMES,
) == _OUTPUT_NAMES + _INPUT_NAMES
@pytest.mark.parametrize(
("layer_index", "num_layers", "shared_layers", "message"),
[
(-1, 6, 3, "valid layer index"),
(6, 6, 3, "valid layer index"),
(0, 0, 1, "valid layer index"),
(0, 6, 7, "cannot exceed"),
],
)
def test_shared_kv_layout_rejects_impossible_ownership(
layer_index,
num_layers,
shared_layers,
message,
):
with pytest.raises(ValueError) as error:
attention_projection_names(
projection_target="all",
layer_index=layer_index,
num_layers=num_layers,
num_kv_shared_layers=shared_layers,
output_names=_OUTPUT_NAMES,
input_names=_INPUT_NAMES,
)
expected = (
"num_kv_shared_layers cannot exceed the model layer count"
if message == "cannot exceed"
else "shared-KV projection requires a valid layer index and layer count"
)
assert str(error.value) == expected
@pytest.mark.parametrize(
("class_name", "has_quant_state", "expected"),
[
("Parameter", False, False),
("Parameter", True, True),
("Params4bit", False, True),
("Int8Params", False, True),
("QuantLinear", False, True),
("WQLinear_GEMM", False, True),
],
)
def test_quantized_parameter_markers_are_closed_and_explicit(
class_name,
has_quant_state,
expected,
):
assert is_quantized_parameter(
class_name=class_name,
has_quant_state=has_quant_state,
) is expected
@pytest.mark.parametrize(
("overrides", "expected"),
[
({}, "float"),
({"data_is_floating_point": False}, "integer"),
({"has_quant_state": True}, "quantized_parameter"),
({"parameter_class_name": "Int8Params"}, "quantized_parameter"),
({"module_class_name": "QuantLinear"}, "packed_module"),
(
{
"module_class_name": "WQLinear_GEMV",
"has_quant_state": True,
"data_is_floating_point": False,
},
"packed_module",
),
],
)
def test_weight_storage_classification_uses_safe_precedence(overrides, expected):
inputs = {
"module_class_name": "Linear",
"parameter_class_name": "Parameter",
"has_quant_state": False,
"data_is_floating_point": True,
}
inputs.update(overrides)
assert classify_weight_storage(**inputs) == expected
@given(st.floats(min_value=0, max_value=10_000, allow_nan=False, allow_infinity=False))
def test_effective_memory_respects_quantization_ratios(estimate_gb):
assert effective_model_memory_gb(estimate_gb, None) == estimate_gb
assert effective_model_memory_gb(estimate_gb, "4bit") == estimate_gb / 4
assert effective_model_memory_gb(estimate_gb, "8bit") == estimate_gb / 2
@pytest.mark.parametrize(
("estimate_gb", "quantization", "available_gb", "expected"),
[
(44.0, "4bit", 16.0, True),
(44.8, "4bit", 16.0, False),
(22.0, "8bit", 16.0, True),
(22.4, "8bit", 16.0, False),
(2.0, "4bit", 1.0, True),
(1.0, None, 16.0, False),
(0.0, "4bit", 16.0, False),
(1.0, "4bit", 0.0, False),
],
)
def test_quantized_fit_requires_positive_memory_and_thirty_percent_headroom(
estimate_gb,
quantization,
available_gb,
expected,
):
assert quantized_model_fits_gpu(estimate_gb, quantization, available_gb) is expected
def _snapshot(**overrides):
values = {
"skip_snapshot": None,
"initial_gpu_free_gb": 0.0,
"remaining_gpu_free_gb": 0.0,
"has_native_quantization": False,
"estimate_gb": 0.0,
"quantization": None,
}
values.update(overrides)
return should_snapshot_model(**values)
def test_explicit_snapshot_choice_overrides_every_memory_signal():
constrained = {
"initial_gpu_free_gb": 1.0,
"remaining_gpu_free_gb": 0.0,
"has_native_quantization": True,
"estimate_gb": 1000.0,
}
assert _snapshot(skip_snapshot=True, **constrained) is False
assert _snapshot(skip_snapshot=False, **constrained) is True
@pytest.mark.parametrize(
("remaining_gb", "expected"),
[(3.99, False), (4.0, True), (4.01, True)],
)
def test_native_quantization_snapshot_boundary_is_forty_percent(remaining_gb, expected):
assert _snapshot(
initial_gpu_free_gb=10.0,
remaining_gpu_free_gb=remaining_gb,
has_native_quantization=True,
) is expected
def test_native_snapshot_policy_uses_any_positive_gpu_signal():
assert _snapshot(
initial_gpu_free_gb=0.5,
remaining_gpu_free_gb=0.1,
has_native_quantization=True,
) is False
assert _snapshot(
initial_gpu_free_gb=0.0,
remaining_gpu_free_gb=-1.0,
has_native_quantization=True,
) is True
@pytest.mark.parametrize(
("estimate_gb", "quantization", "expected"),
[(5.0, None, True), (5.01, None, False), (20.0, "4bit", True), (20.04, "4bit", False)],
)
def test_estimated_snapshot_boundary_is_half_of_free_memory(
estimate_gb,
quantization,
expected,
):
assert _snapshot(
initial_gpu_free_gb=10.0,
estimate_gb=estimate_gb,
quantization=quantization,
) is expected
def test_estimated_snapshot_policy_uses_any_positive_gpu_signal():
assert _snapshot(initial_gpu_free_gb=0.5, estimate_gb=0.3) is False
assert _snapshot(initial_gpu_free_gb=0.0, estimate_gb=1.0) is True
def test_snapshot_defaults_to_enabled_without_a_positive_gpu_signal():
assert _snapshot() is True
assert _snapshot(initial_gpu_free_gb=-1.0, estimate_gb=100.0) is True
@pytest.mark.parametrize(
("kwargs", "expected"),
[
({"is_moe": False, "total_params_b": 999, "num_experts": 999}, "dense"),
({"is_moe": True, "total_params_b": 99.99, "num_experts": 999}, "small_moe"),
({"is_moe": True, "total_params_b": 0.5, "num_experts": 999}, "small_moe"),
({"is_moe": True, "total_params_b": 100.0, "num_experts": 1}, "large_moe"),
({"is_moe": True, "total_params_b": 0.0, "num_experts": 1}, "small_moe"),
({"is_moe": True, "total_params_b": 0.0, "num_experts": 16}, "small_moe"),
({"is_moe": True, "total_params_b": 0.0, "num_experts": 17}, "large_moe"),
],
)
def test_architecture_classification_precedence_and_boundaries(kwargs, expected):
assert classify_architecture_size(
model_name="custom/model",
large_moe_name_patterns=("large-model",),
**kwargs,
) == expected
def test_architecture_name_fallback_is_case_insensitive_and_conservative():
common = {
"is_moe": True,
"total_params_b": 0.0,
"num_experts": 0,
"large_moe_name_patterns": ("giant-moe",),
}
assert classify_architecture_size(model_name="ORG/GIANT-MOE-V1", **common) == "large_moe"
assert classify_architecture_size(model_name="org/unknown-moe", **common) == "small_moe"
def test_single_expert_precedes_the_large_name_fallback():
assert classify_architecture_size(
is_moe=True,
total_params_b=0.0,
num_experts=1,
model_name="org/giant-moe-v1",
large_moe_name_patterns=("giant-moe",),
) == "small_moe"
@pytest.mark.parametrize(
("target", "cuda_available", "cuda_major", "version", "expected"),
[
("cuda", True, 8, "2.0.0", True),
("cuda:1", True, 7, "2.0.0", False),
("cuda", False, None, "2.0.0", False),
("mps", False, None, "2.2.9", False),
("mps", False, None, "2.3.0", True),
("mps", False, None, "2.10.0", True),
("cpu", True, 1, "1.0.0", True),
],
)
def test_bfloat16_capability_boundaries(
target,
cuda_available,
cuda_major,
version,
expected,
):
assert supports_bfloat16_target(
target,
cuda_available=cuda_available,
cuda_major=cuda_major,
torch_version=version,
) is expected