564 lines
19 KiB
Python
564 lines
19 KiB
Python
"""Framework-independent capability declaration and compatibility tests."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import FrozenInstanceError, replace
|
|
from typing import Any, cast
|
|
|
|
import pytest
|
|
from comfyui_sigmax.core import (
|
|
CapabilityCompatibilityError,
|
|
CapabilityDimension,
|
|
CompatibilityDecision,
|
|
CompatibilityLevel,
|
|
CompatibilityReason,
|
|
ExecutionBehavior,
|
|
ExecutionFeatureRequest,
|
|
ModelCapabilities,
|
|
NoiseOwnership,
|
|
PredictionType,
|
|
ProfileCapabilities,
|
|
SamplerCapabilities,
|
|
SamplerState,
|
|
ScheduleContractError,
|
|
ScheduleOwnership,
|
|
SigmaDomain,
|
|
TerminalRequirement,
|
|
TerminalSigma,
|
|
evaluate_compatibility,
|
|
require_compatible,
|
|
)
|
|
|
|
|
|
def _model() -> ModelCapabilities:
|
|
return ModelCapabilities(
|
|
model_family="krea2",
|
|
model_variant="turbo",
|
|
accepted_prediction_types=(PredictionType.FLOW_VELOCITY,),
|
|
accepted_sigma_domains=(SigmaDomain.UNIT_FLOW,),
|
|
accepted_ownerships=(ScheduleOwnership.EXTERNAL_SIGMAS,),
|
|
supports_partial_denoise=True,
|
|
supports_per_token_timesteps=False,
|
|
)
|
|
|
|
|
|
def _profile() -> ProfileCapabilities:
|
|
return ProfileCapabilities(
|
|
profile_id="krea2.turbo.official",
|
|
profile_version="1",
|
|
model_family="krea2",
|
|
model_variant="turbo",
|
|
prediction_type=PredictionType.FLOW_VELOCITY,
|
|
sigma_domain=SigmaDomain.UNIT_FLOW,
|
|
ownership=ScheduleOwnership.EXTERNAL_SIGMAS,
|
|
terminal_sigma=TerminalSigma.ZERO,
|
|
allowed_execution_behaviors=(ExecutionBehavior.DETERMINISTIC,),
|
|
allowed_noise_ownerships=(NoiseOwnership.NONE,),
|
|
allowed_sampler_state=(SamplerState.STEP_INDEX,),
|
|
supports_partial_denoise=True,
|
|
supports_per_token_timesteps=False,
|
|
reference_sampler_ids=("comfy.euler",),
|
|
)
|
|
|
|
|
|
def _sampler() -> SamplerCapabilities:
|
|
return SamplerCapabilities(
|
|
sampler_id="comfy.euler",
|
|
sampler_version="native",
|
|
accepted_prediction_types=(PredictionType.FLOW_VELOCITY,),
|
|
accepted_sigma_domains=(SigmaDomain.UNIT_FLOW,),
|
|
accepted_ownerships=(ScheduleOwnership.EXTERNAL_SIGMAS,),
|
|
terminal_requirement=TerminalRequirement.REQUIRES_ZERO,
|
|
execution_behavior=ExecutionBehavior.DETERMINISTIC,
|
|
noise_ownership=NoiseOwnership.NONE,
|
|
required_state=(),
|
|
supports_partial_denoise=True,
|
|
supports_per_token_timesteps=False,
|
|
)
|
|
|
|
|
|
def _decision(
|
|
*,
|
|
model: ModelCapabilities | None = None,
|
|
profile: ProfileCapabilities | None = None,
|
|
sampler: SamplerCapabilities | None = None,
|
|
request: ExecutionFeatureRequest | None = None,
|
|
) -> CompatibilityDecision:
|
|
return evaluate_compatibility(
|
|
model=model or _model(),
|
|
profile=profile or _profile(),
|
|
sampler=sampler or _sampler(),
|
|
request=request or ExecutionFeatureRequest(),
|
|
)
|
|
|
|
|
|
def test_krea_like_native_euler_capabilities_allow_execution() -> None:
|
|
decision = _decision()
|
|
|
|
assert decision.level is CompatibilityLevel.ALLOW
|
|
assert decision.reasons == (CompatibilityReason.COMPATIBLE,)
|
|
assert decision.considered == tuple(CapabilityDimension)
|
|
assert require_compatible(decision) is decision
|
|
|
|
|
|
def test_capability_compatible_nonreference_sampler_warns() -> None:
|
|
decision = _decision(sampler=replace(_sampler(), sampler_id="other.euler"))
|
|
|
|
assert decision.level is CompatibilityLevel.WARN
|
|
assert decision.reasons == (CompatibilityReason.SAMPLER_NOT_PROFILE_REFERENCE,)
|
|
assert require_compatible(decision) is decision
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("terminal", "requirement"),
|
|
[
|
|
(TerminalSigma.ZERO, TerminalRequirement.ACCEPTS_EITHER),
|
|
(TerminalSigma.NONZERO, TerminalRequirement.ACCEPTS_EITHER),
|
|
(TerminalSigma.NONZERO, TerminalRequirement.FORBIDS_ZERO),
|
|
],
|
|
)
|
|
def test_compatible_terminal_policies_allow(
|
|
terminal: TerminalSigma,
|
|
requirement: TerminalRequirement,
|
|
) -> None:
|
|
decision = _decision(
|
|
profile=replace(_profile(), terminal_sigma=terminal),
|
|
sampler=replace(_sampler(), terminal_requirement=requirement),
|
|
)
|
|
|
|
assert decision.level is CompatibilityLevel.ALLOW
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("model", "profile", "sampler", "reason"),
|
|
[
|
|
(
|
|
replace(_model(), model_family="other"),
|
|
_profile(),
|
|
_sampler(),
|
|
CompatibilityReason.MODEL_FAMILY_MISMATCH,
|
|
),
|
|
(
|
|
replace(_model(), model_variant="raw"),
|
|
_profile(),
|
|
_sampler(),
|
|
CompatibilityReason.MODEL_VARIANT_MISMATCH,
|
|
),
|
|
(
|
|
replace(
|
|
_model(),
|
|
accepted_prediction_types=(PredictionType.EPSILON,),
|
|
),
|
|
_profile(),
|
|
_sampler(),
|
|
CompatibilityReason.MODEL_PREDICTION_UNSUPPORTED,
|
|
),
|
|
(
|
|
replace(
|
|
_model(),
|
|
accepted_sigma_domains=(SigmaDomain.CONTINUOUS_EDM,),
|
|
),
|
|
_profile(),
|
|
_sampler(),
|
|
CompatibilityReason.MODEL_SIGMA_DOMAIN_UNSUPPORTED,
|
|
),
|
|
(
|
|
replace(
|
|
_model(),
|
|
accepted_ownerships=(ScheduleOwnership.MODEL_NATIVE,),
|
|
),
|
|
_profile(),
|
|
_sampler(),
|
|
CompatibilityReason.MODEL_OWNERSHIP_UNSUPPORTED,
|
|
),
|
|
(
|
|
_model(),
|
|
_profile(),
|
|
replace(
|
|
_sampler(),
|
|
accepted_prediction_types=(PredictionType.EPSILON,),
|
|
),
|
|
CompatibilityReason.SAMPLER_PREDICTION_UNSUPPORTED,
|
|
),
|
|
(
|
|
_model(),
|
|
_profile(),
|
|
replace(
|
|
_sampler(),
|
|
accepted_sigma_domains=(SigmaDomain.CONTINUOUS_EDM,),
|
|
),
|
|
CompatibilityReason.SAMPLER_SIGMA_DOMAIN_UNSUPPORTED,
|
|
),
|
|
(
|
|
_model(),
|
|
_profile(),
|
|
replace(
|
|
_sampler(),
|
|
accepted_ownerships=(ScheduleOwnership.MODEL_NATIVE,),
|
|
),
|
|
CompatibilityReason.SAMPLER_OWNERSHIP_UNSUPPORTED,
|
|
),
|
|
(
|
|
_model(),
|
|
_profile(),
|
|
replace(
|
|
_sampler(),
|
|
terminal_requirement=TerminalRequirement.FORBIDS_ZERO,
|
|
),
|
|
CompatibilityReason.TERMINAL_REQUIREMENT_MISMATCH,
|
|
),
|
|
(
|
|
_model(),
|
|
_profile(),
|
|
replace(
|
|
_sampler(),
|
|
execution_behavior=ExecutionBehavior.STOCHASTIC,
|
|
noise_ownership=NoiseOwnership.SAMPLER,
|
|
),
|
|
CompatibilityReason.EXECUTION_BEHAVIOR_MISMATCH,
|
|
),
|
|
(
|
|
_model(),
|
|
replace(
|
|
_profile(),
|
|
allowed_execution_behaviors=(
|
|
ExecutionBehavior.DETERMINISTIC,
|
|
ExecutionBehavior.STOCHASTIC,
|
|
),
|
|
allowed_noise_ownerships=(NoiseOwnership.NONE, NoiseOwnership.CALLER),
|
|
),
|
|
replace(
|
|
_sampler(),
|
|
execution_behavior=ExecutionBehavior.STOCHASTIC,
|
|
noise_ownership=NoiseOwnership.SAMPLER,
|
|
),
|
|
CompatibilityReason.NOISE_OWNERSHIP_MISMATCH,
|
|
),
|
|
(
|
|
_model(),
|
|
_profile(),
|
|
replace(_sampler(), required_state=(SamplerState.MULTISTEP_HISTORY,)),
|
|
CompatibilityReason.SAMPLER_STATE_UNSUPPORTED,
|
|
),
|
|
],
|
|
)
|
|
def test_each_model_profile_sampler_semantic_mismatch_rejects(
|
|
model: ModelCapabilities,
|
|
profile: ProfileCapabilities,
|
|
sampler: SamplerCapabilities,
|
|
reason: CompatibilityReason,
|
|
) -> None:
|
|
decision = _decision(model=model, profile=profile, sampler=sampler)
|
|
|
|
assert decision.level is CompatibilityLevel.REJECT
|
|
assert reason in decision.reasons
|
|
with pytest.raises(CapabilityCompatibilityError, match=reason.value):
|
|
require_compatible(decision)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("feature", "component", "reason"),
|
|
[
|
|
(
|
|
"partial",
|
|
"model",
|
|
CompatibilityReason.PARTIAL_DENOISE_UNSUPPORTED_BY_MODEL,
|
|
),
|
|
(
|
|
"partial",
|
|
"profile",
|
|
CompatibilityReason.PARTIAL_DENOISE_UNSUPPORTED_BY_PROFILE,
|
|
),
|
|
(
|
|
"partial",
|
|
"sampler",
|
|
CompatibilityReason.PARTIAL_DENOISE_UNSUPPORTED_BY_SAMPLER,
|
|
),
|
|
(
|
|
"per_token",
|
|
"model",
|
|
CompatibilityReason.PER_TOKEN_TIMESTEPS_UNSUPPORTED_BY_MODEL,
|
|
),
|
|
(
|
|
"per_token",
|
|
"profile",
|
|
CompatibilityReason.PER_TOKEN_TIMESTEPS_UNSUPPORTED_BY_PROFILE,
|
|
),
|
|
(
|
|
"per_token",
|
|
"sampler",
|
|
CompatibilityReason.PER_TOKEN_TIMESTEPS_UNSUPPORTED_BY_SAMPLER,
|
|
),
|
|
],
|
|
)
|
|
def test_requested_execution_feature_requires_three_party_support(
|
|
feature: str,
|
|
component: str,
|
|
reason: CompatibilityReason,
|
|
) -> None:
|
|
model = _model()
|
|
profile = _profile()
|
|
sampler = _sampler()
|
|
if feature == "partial":
|
|
request = ExecutionFeatureRequest(use_partial_denoise=True)
|
|
if component == "model":
|
|
model = replace(model, supports_partial_denoise=False)
|
|
elif component == "profile":
|
|
profile = replace(profile, supports_partial_denoise=False)
|
|
else:
|
|
sampler = replace(sampler, supports_partial_denoise=False)
|
|
else:
|
|
request = ExecutionFeatureRequest(use_per_token_timesteps=True)
|
|
model = replace(model, supports_per_token_timesteps=True)
|
|
profile = replace(profile, supports_per_token_timesteps=True)
|
|
sampler = replace(sampler, supports_per_token_timesteps=True)
|
|
if component == "model":
|
|
model = replace(model, supports_per_token_timesteps=False)
|
|
elif component == "profile":
|
|
profile = replace(profile, supports_per_token_timesteps=False)
|
|
else:
|
|
sampler = replace(sampler, supports_per_token_timesteps=False)
|
|
|
|
decision = _decision(model=model, profile=profile, sampler=sampler, request=request)
|
|
|
|
assert decision.level is CompatibilityLevel.REJECT
|
|
assert reason in decision.reasons
|
|
|
|
|
|
def test_supported_partial_and_per_token_features_allow() -> None:
|
|
request = ExecutionFeatureRequest(
|
|
use_partial_denoise=True,
|
|
use_per_token_timesteps=True,
|
|
)
|
|
decision = _decision(
|
|
model=replace(_model(), supports_per_token_timesteps=True),
|
|
profile=replace(_profile(), supports_per_token_timesteps=True),
|
|
sampler=replace(_sampler(), supports_per_token_timesteps=True),
|
|
request=request,
|
|
)
|
|
|
|
assert decision.level is CompatibilityLevel.ALLOW
|
|
|
|
|
|
def test_multiple_rejections_have_stable_evaluator_order_and_no_warning() -> None:
|
|
decision = _decision(
|
|
model=replace(
|
|
_model(),
|
|
model_family="other",
|
|
accepted_prediction_types=(PredictionType.EPSILON,),
|
|
supports_partial_denoise=False,
|
|
),
|
|
sampler=replace(_sampler(), sampler_id="other.euler", supports_partial_denoise=False),
|
|
request=ExecutionFeatureRequest(use_partial_denoise=True),
|
|
)
|
|
|
|
assert decision.level is CompatibilityLevel.REJECT
|
|
assert decision.reasons == (
|
|
CompatibilityReason.MODEL_FAMILY_MISMATCH,
|
|
CompatibilityReason.MODEL_PREDICTION_UNSUPPORTED,
|
|
CompatibilityReason.PARTIAL_DENOISE_UNSUPPORTED_BY_MODEL,
|
|
CompatibilityReason.PARTIAL_DENOISE_UNSUPPORTED_BY_SAMPLER,
|
|
)
|
|
|
|
|
|
def test_rejected_gate_stops_before_execution_sentinel() -> None:
|
|
executed: list[bool] = []
|
|
decision = _decision(
|
|
sampler=replace(
|
|
_sampler(),
|
|
accepted_sigma_domains=(SigmaDomain.CONTINUOUS_EDM,),
|
|
)
|
|
)
|
|
|
|
def guarded_execution() -> None:
|
|
require_compatible(decision)
|
|
executed.append(True)
|
|
|
|
with pytest.raises(CapabilityCompatibilityError):
|
|
guarded_execution()
|
|
assert executed == []
|
|
|
|
|
|
def test_capability_contracts_are_frozen() -> None:
|
|
model = _model()
|
|
decision = _decision()
|
|
|
|
with pytest.raises(FrozenInstanceError):
|
|
model.model_variant = "raw" # type: ignore[misc]
|
|
with pytest.raises(FrozenInstanceError):
|
|
decision.level = CompatibilityLevel.REJECT # type: ignore[misc]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"factory",
|
|
[
|
|
lambda: replace(_model(), model_family=""),
|
|
lambda: replace(_model(), model_family="Krea2"),
|
|
lambda: replace(_model(), model_family="x" * 257),
|
|
lambda: replace(_model(), supports_partial_denoise=cast(Any, 1)),
|
|
lambda: replace(_model(), accepted_prediction_types=cast(Any, [])),
|
|
lambda: replace(_model(), accepted_prediction_types=()),
|
|
lambda: replace(
|
|
_model(),
|
|
accepted_prediction_types=(
|
|
PredictionType.FLOW_VELOCITY,
|
|
PredictionType.FLOW_VELOCITY,
|
|
),
|
|
),
|
|
lambda: replace(
|
|
_model(),
|
|
accepted_prediction_types=(
|
|
PredictionType.EPSILON,
|
|
PredictionType.FLOW_VELOCITY,
|
|
),
|
|
),
|
|
lambda: replace(
|
|
_model(),
|
|
accepted_prediction_types=cast(Any, ("flow_velocity",)),
|
|
),
|
|
lambda: replace(_profile(), reference_sampler_ids=("",)),
|
|
lambda: replace(_profile(), reference_sampler_ids=cast(Any, [])),
|
|
lambda: replace(_profile(), profile_version="x" * 257),
|
|
lambda: replace(_profile(), prediction_type=cast(Any, "flow_velocity")),
|
|
lambda: replace(_profile(), sigma_domain=cast(Any, "unit_flow")),
|
|
lambda: replace(_profile(), ownership=cast(Any, "external_sigmas")),
|
|
lambda: replace(_profile(), terminal_sigma=cast(Any, "zero")),
|
|
lambda: replace(
|
|
_profile(),
|
|
reference_sampler_ids=("comfy.euler", "comfy.euler"),
|
|
),
|
|
lambda: replace(
|
|
_profile(),
|
|
reference_sampler_ids=("z.euler", "a.euler"),
|
|
),
|
|
lambda: replace(
|
|
_profile(),
|
|
allowed_execution_behaviors=(ExecutionBehavior.DETERMINISTIC,),
|
|
allowed_noise_ownerships=(NoiseOwnership.SAMPLER,),
|
|
),
|
|
lambda: replace(
|
|
_profile(),
|
|
allowed_execution_behaviors=(ExecutionBehavior.STOCHASTIC,),
|
|
allowed_noise_ownerships=(NoiseOwnership.NONE,),
|
|
),
|
|
lambda: replace(
|
|
_profile(),
|
|
allowed_execution_behaviors=(
|
|
ExecutionBehavior.DETERMINISTIC,
|
|
ExecutionBehavior.STOCHASTIC,
|
|
),
|
|
allowed_noise_ownerships=(NoiseOwnership.NONE,),
|
|
),
|
|
lambda: replace(
|
|
_profile(),
|
|
allowed_execution_behaviors=(ExecutionBehavior.DETERMINISTIC,),
|
|
allowed_noise_ownerships=(NoiseOwnership.NONE, NoiseOwnership.CALLER),
|
|
),
|
|
lambda: replace(
|
|
_sampler(),
|
|
terminal_requirement=cast(Any, "requires_zero"),
|
|
),
|
|
lambda: replace(
|
|
_sampler(),
|
|
execution_behavior=cast(Any, "deterministic"),
|
|
),
|
|
lambda: replace(
|
|
_sampler(),
|
|
noise_ownership=cast(Any, "none"),
|
|
),
|
|
lambda: replace(
|
|
_sampler(),
|
|
execution_behavior=ExecutionBehavior.DETERMINISTIC,
|
|
noise_ownership=NoiseOwnership.SAMPLER,
|
|
),
|
|
lambda: replace(
|
|
_sampler(),
|
|
execution_behavior=ExecutionBehavior.STOCHASTIC,
|
|
noise_ownership=NoiseOwnership.NONE,
|
|
),
|
|
lambda: ExecutionFeatureRequest(use_partial_denoise=cast(Any, 1)),
|
|
],
|
|
)
|
|
def test_invalid_capability_declarations_fail_closed(factory: Any) -> None:
|
|
with pytest.raises(ScheduleContractError):
|
|
factory()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"factory",
|
|
[
|
|
lambda: CompatibilityDecision(
|
|
level=CompatibilityLevel.ALLOW,
|
|
considered=tuple(CapabilityDimension),
|
|
reasons=(CompatibilityReason.SAMPLER_NOT_PROFILE_REFERENCE,),
|
|
),
|
|
lambda: CompatibilityDecision(
|
|
level=CompatibilityLevel.WARN,
|
|
considered=tuple(CapabilityDimension),
|
|
reasons=(CompatibilityReason.COMPATIBLE,),
|
|
),
|
|
lambda: CompatibilityDecision(
|
|
level=CompatibilityLevel.REJECT,
|
|
considered=tuple(CapabilityDimension),
|
|
reasons=(CompatibilityReason.SAMPLER_NOT_PROFILE_REFERENCE,),
|
|
),
|
|
lambda: CompatibilityDecision(
|
|
level=CompatibilityLevel.REJECT,
|
|
considered=tuple(CapabilityDimension),
|
|
reasons=(
|
|
CompatibilityReason.MODEL_FAMILY_MISMATCH,
|
|
CompatibilityReason.MODEL_FAMILY_MISMATCH,
|
|
),
|
|
),
|
|
lambda: CompatibilityDecision(
|
|
level=CompatibilityLevel.REJECT,
|
|
considered=(),
|
|
reasons=(CompatibilityReason.MODEL_FAMILY_MISMATCH,),
|
|
),
|
|
lambda: CompatibilityDecision(
|
|
level=cast(Any, "allow"),
|
|
considered=tuple(CapabilityDimension),
|
|
reasons=(CompatibilityReason.COMPATIBLE,),
|
|
),
|
|
lambda: CompatibilityDecision(
|
|
level=CompatibilityLevel.REJECT,
|
|
considered=cast(Any, list(CapabilityDimension)),
|
|
reasons=(CompatibilityReason.MODEL_FAMILY_MISMATCH,),
|
|
),
|
|
lambda: CompatibilityDecision(
|
|
level=CompatibilityLevel.REJECT,
|
|
considered=tuple(reversed(tuple(CapabilityDimension))),
|
|
reasons=(CompatibilityReason.MODEL_FAMILY_MISMATCH,),
|
|
),
|
|
lambda: CompatibilityDecision(
|
|
level=CompatibilityLevel.REJECT,
|
|
considered=tuple(CapabilityDimension)[:-1],
|
|
reasons=(CompatibilityReason.MODEL_FAMILY_MISMATCH,),
|
|
),
|
|
],
|
|
)
|
|
def test_invalid_compatibility_decisions_fail_closed(factory: Any) -> None:
|
|
with pytest.raises(ScheduleContractError):
|
|
factory()
|
|
|
|
|
|
def test_evaluator_rejects_noncontract_arguments() -> None:
|
|
valid_arguments = {
|
|
"model": _model(),
|
|
"profile": _profile(),
|
|
"sampler": _sampler(),
|
|
"request": ExecutionFeatureRequest(),
|
|
}
|
|
for name, expected in (
|
|
("model", "ModelCapabilities"),
|
|
("profile", "ProfileCapabilities"),
|
|
("sampler", "SamplerCapabilities"),
|
|
("request", "ExecutionFeatureRequest"),
|
|
):
|
|
invalid_arguments = {**valid_arguments, name: object()}
|
|
with pytest.raises(ScheduleContractError, match=expected):
|
|
evaluate_compatibility(**cast(Any, invalid_arguments))
|
|
|
|
with pytest.raises(ScheduleContractError, match="CompatibilityDecision"):
|
|
require_compatible(cast(Any, object()))
|