Phase 3 of the modernization plan: move the framework-free math out of the comfy-coupled modules so Tier-0 tests can run on plain Linux without ComfyUI, coremltools, or python_coreml_stable_diffusion. New pure-core package (no comfy / coreml / mps imports): - coreml_suite.core.latents: chunk_batch, merge_chunks - coreml_suite.core.controlnet: expand_inputs, no_control, extract_residual_kwargs, chunk_control - coreml_suite.core.inputs: CoreMLInputs (chunks + coreml_kwargs) - coreml_suite.core.sdxl: is_sdxl / is_sdxl_base / is_sdxl_refiner, build_sdxl_time_ids (base len 6, refiner len 5), build_sdxl_text_embeds, sdxl_model_function_wrapper - coreml_suite.core.naming: compose_out_name, lora_names_from_params Thin adapters keep the public import paths: - coreml_suite.latents / coreml_suite.controlnet: re-export from core - coreml_suite.models: CoreMLModelWrapper, CoreMLModelWrapperLCM, add_sdxl_model_options (now uses the pure builders from core.sdxl), get_latent_image, get_model_patcher remain framework-coupled - coreml_suite.nodes: CoreMLConverter.convert now delegates the out_name composition to core.naming.compose_out_name Test infra: - tests/unit/* re-pointed at coreml_suite.core.* - test_chunks.py dropped `from comfy.model_management import ...` and the dead `model_config` fixture (Phase 1 left it broken; Phase 3 removes it entirely) - test_characterization_sdxl_options now targets the pure builders directly via inspect.getclosurevars on the wrapper closure - test_characterization_out_name now calls compose_out_name without the heavy CoreMLConverter monkey-patching that Phase 2 needed - tests/unit/test_tier0_purity.py: new gate that fails if comfy / coremltools / etc leak into sys.modules during a pure `-m unit` run (skipped in mixed runs where m2 / integration legitimately import them) - tests/__init__.py + top-level conftest.py + pyproject addopts `--import-mode=importlib --confcutdir=tests` together stop pytest from importing the repo-root `__init__.py` (the ComfyUI custom-node entry pulls in comfy) - tests/conftest.py adds tier-aware collect_ignore so `-m unit` skips tests/m2 + tests/integration at collection time Verification: - `pytest -m unit tests/` → 88 passed in ~2s; deterministic across runs - Tier-0 purity gate confirms no comfy/coreml/etc in sys.modules - m2 golden image (Phase 2 anchor) still hashes identical → refactor produced bit-for-bit unchanged output - `git diff main -- __init__.py coreml_suite/nodes.py` shows zero churn to NODE_CLASS_MAPPINGS keys or INPUT_TYPES field names (public workflow contract intact)
124 lines
3.5 KiB
Python
124 lines
3.5 KiB
Python
"""Original smoke tests, re-pointed at coreml_suite.core.* in Phase 3.
|
|
|
|
Drops the `from comfy.model_management import get_torch_device` import
|
|
(replaced with torch.device('cpu') so Tier 0 runs without ComfyUI) and the
|
|
dead `model_config` fixture (defined in Phase 1 but never consumed).
|
|
"""
|
|
import pytest
|
|
import torch
|
|
|
|
from coreml_suite.core.controlnet import chunk_control
|
|
from coreml_suite.core.inputs import CoreMLInputs
|
|
from coreml_suite.core.latents import chunk_batch, merge_chunks
|
|
|
|
|
|
CPU = torch.device("cpu")
|
|
|
|
|
|
@pytest.fixture
|
|
def expected_inputs():
|
|
return {
|
|
"sample": {"shape": (2, 4, 64, 64)},
|
|
"timestep": {"shape": (2,)},
|
|
"timestep_cond": {"shape": (2, 256)},
|
|
"encoder_hidden_states": {"shape": (2, 768, 1, 77)},
|
|
"additional_residual_0": {"shape": (2, 320, 64, 64)},
|
|
"additional_residual_1": {"shape": (2, 640, 32, 32)},
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize("batch_size", [1, 2, 4, 5, 9])
|
|
def test_batch_chunking(batch_size):
|
|
latent_image = torch.randn(batch_size, 4, 64, 64).to(CPU)
|
|
target_shape = (4, 4, 64, 64)
|
|
|
|
chunked = chunk_batch(latent_image, target_shape)
|
|
|
|
for chunk in chunked:
|
|
assert chunk.shape == target_shape
|
|
|
|
if batch_size % target_shape[0] != 0:
|
|
assert chunked[-1][batch_size % target_shape[0] :].sum() == 0
|
|
|
|
|
|
@pytest.mark.parametrize("batch_size", [1, 2, 4, 5, 9])
|
|
def test_merge_chunks(batch_size):
|
|
input_tensor = torch.randn(batch_size, 4, 64, 64).to(CPU)
|
|
target_shape = (4, 4, 64, 64)
|
|
chunked = chunk_batch(input_tensor, target_shape)
|
|
|
|
merged = merge_chunks(chunked, input_tensor.shape)
|
|
|
|
assert merged.shape == input_tensor.shape
|
|
assert torch.equal(input_tensor, merged)
|
|
|
|
|
|
@pytest.fixture
|
|
def inputs():
|
|
x = torch.randn(1, 4, 64, 64).to(CPU)
|
|
t = torch.randn([1]).to(CPU)
|
|
c_crossattn = torch.randn(1, 77, 768).to(CPU)
|
|
control = {
|
|
"output": [
|
|
torch.randn(1, 320, 64, 64).to(CPU),
|
|
torch.randn(1, 640, 32, 32).to(CPU),
|
|
],
|
|
}
|
|
timestep_cond = torch.randn(1, 256).to(CPU)
|
|
|
|
return CoreMLInputs(x, t, c_crossattn, control, timestep_cond=timestep_cond)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"b, target_size, num_chunks",
|
|
[
|
|
(1, 2, 1),
|
|
(1, 1, 1),
|
|
(2, 2, 1),
|
|
(3, 2, 2),
|
|
(4, 2, 2),
|
|
(5, 3, 2),
|
|
(9, 4, 3),
|
|
],
|
|
)
|
|
def test_chunking_controlnet(b, target_size, num_chunks):
|
|
cn = {
|
|
"output": [
|
|
torch.randn(b, 320, 64, 64).to(CPU),
|
|
torch.randn(b, 640, 32, 32).to(CPU),
|
|
],
|
|
"middle": [
|
|
torch.randn(b, 1280, 8, 8).to(CPU),
|
|
],
|
|
}
|
|
|
|
chunked = chunk_control(cn, target_size)
|
|
|
|
assert len(chunked) == num_chunks
|
|
for chunk in chunked:
|
|
assert chunk["output"][0].shape == (target_size, 320, 64, 64)
|
|
assert chunk["output"][1].shape == (target_size, 640, 32, 32)
|
|
assert chunk["middle"][0].shape == (target_size, 1280, 8, 8)
|
|
|
|
|
|
def test_chunking_no_control():
|
|
cn = None
|
|
target_size = 2
|
|
|
|
chunked = chunk_control(cn, target_size)
|
|
|
|
assert chunked == [None, None]
|
|
|
|
|
|
def test_chunking_inputs(expected_inputs, inputs):
|
|
chunked = inputs.chunks(expected_inputs)
|
|
|
|
assert len(chunked) == 1
|
|
|
|
assert chunked[0].x.shape == (2, 4, 64, 64)
|
|
assert chunked[0].t.shape == (2,)
|
|
assert chunked[0].context.shape == (2, 77, 768)
|
|
assert chunked[0].control["output"][0].shape == (2, 320, 64, 64)
|
|
assert chunked[0].control["output"][1].shape == (2, 640, 32, 32)
|
|
assert chunked[0].ts_cond.shape == (2, 256)
|