Files
Ethanfel 011c915fa7 release: ship H3 correctness fixes and research tools in 1.8.5
Publish the existing H3 merge/export and autotuner correctness fixes, offline character and AV calibration tools, provenance audits, and regression coverage. Add a dictionary-preserving storage hook for offline mapped TIES exports.

Keep scoring weights and defaults unchanged, retain NP/CT as opt-in, and exclude the rejected F08 search-admission prototype. Validate the exact staged tree: 1052 Python tests, 69 subtests, 3 JS tests, and real ComfyUI CPU export round trips pass; 3 CUDA-only tests skipped.
2026-09-09 12:10:02 +02:00

252 lines
13 KiB
Python

"""Full-target export fidelity through real ComfyUI's native key mapping.
Unlike the original pilot checker, this accepts Musubi and AI-Toolkit sources
without borrowing the optimizer's key normalizer. Only base-independent 2D
LoRA factors are supported. This checks serialization fidelity, not AV quality.
"""
import argparse
import importlib.util
import json
import math
from pathlib import Path
import sys
import time
import types
try:
from .h3_benchmark import digest, save_new
from .h3_merge_study import header
except ImportError:
from h3_benchmark import digest, save_new
from h3_merge_study import header
def factor_pair(patch, rows, device):
"""Preserve raw rank alpha and signed up/down values in FP32."""
up, down, alpha, mid, dora, reshape = patch.weights
if mid is not None or dora is not None or reshape is not None:
raise ValueError("Native checker supports plain linear LoRA only")
if up.ndim != 2 or down.ndim != 2 or up.shape[1] != down.shape[0]:
raise ValueError("Invalid linear factor shapes")
scale = float(alpha) / down.shape[0] if alpha is not None else 1.
return up[rows].to(device=device).float() * scale, down.to(device=device).float()
def component_rows(target, shape):
if len(shape) != 2:
raise ValueError("Only linear target matrices are supported")
splits = 3 if target.endswith(".qkv_proj.weight") else 1
if shape[0] % splits:
raise ValueError("Invalid native QKV output shape")
return [slice(i * shape[0] // splits, (i + 1) * shape[0] // splits) for i in range(splits)]
def require_coverage(sources, exported):
expected = set().union(*(set(source) for source in sources))
if not expected or set(exported) != expected:
raise ValueError(f"Native target coverage mismatch: missing={sorted(expected-set(exported))}, extra={sorted(set(exported)-expected)}")
def effective_mode(metadata):
# The existing export stores the auto-detected strategy in merge_mode,
# alongside the explicit optimization_mode that can override it.
# Do not misinterpret an additive export as a weighted average.
if metadata.get("merge_optimization_mode") == "additive":
if json.loads(metadata.get("merge_experimental", "null")) is not None:
raise ValueError("Additive export carries an experimental config")
return "weighted_sum"
if metadata.get("merge_optimization_mode") == "per_prefix":
return "per_prefix"
return metadata["merge_mode"]
def component_label(target, component):
prefix = target.removesuffix(".weight")
if prefix.endswith(".qkv_proj"):
return prefix.removesuffix("qkv_proj") + ("to_q", "to_k", "to_v")[component]
if component != 0:
raise ValueError("Non-QKV target cannot have multiple components")
return prefix
def replay_policy(manifest, metadata):
"""Refuse unsupported winner semantics rather than audit an easier blend."""
selected = manifest["selected"]
config = selected["config"]
decisions = manifest["replay_per_prefix_decisions"]
if (manifest["action"] != "replay" or config["optimization_mode"] != "per_prefix"
or not decisions or decisions != selected["per_prefix_decisions"]
or set(decisions.values()) - {"weighted_sum", "weighted_average", "normalize", "slerp"}
or config["sparsification"] != "disabled" or config["merge_refinement"] != "none"
or config.get("experimental") is not None):
raise ValueError("Winner needs a different numerical reference")
for key in ("sparsification", "merge_refinement", "auto_strength", "optimization_mode"):
if metadata["merge_" + key] != config[key]:
raise ValueError("Replay metadata/config mismatch")
scale = 1.
if config["auto_strength"] == "enabled":
info = manifest["replay_auto_strength"]
scale = info["model_scale"]
if (not math.isfinite(scale) or not 0 <= scale <= 1
or info["original_model_strengths"] != manifest["strengths"]
or info["model_strengths"] != [w * scale for w in manifest["strengths"]]):
raise ValueError("Invalid captured automatic strength")
elif config["auto_strength"] != "disabled" or manifest["replay_auto_strength"] is not None:
raise ValueError("Unexpected automatic strength policy")
return decisions, scale
def load_native(path, mapping, torch, comfy_lora):
from safetensors.torch import load_file
sd = load_file(str(path), device="cpu")
if any(not torch.isfinite(t).all().item() for t in sd.values()):
raise ValueError(f"Non-finite adapter tensors: {path}")
patches = comfy_lora.load_lora(sd, mapping)
# The selected study exports and sources contain only plain LoRA factors.
# Refuse dense/other adapters explicitly rather than silently omitting them.
if any(not hasattr(p, "weights") for p in patches.values()):
raise ValueError("This checker requires native low-rank export factors")
consumed = set().union(*(p.loaded_keys for p in patches.values()))
if consumed != set(sd):
raise ValueError(f"Unconsumed native loader keys: {sorted(set(sd)-consumed)}")
return patches
def audit(run, comfy):
output = run / "export_check.json"
if output.exists():
raise FileExistsError(output)
manifest_path = run / "manifest.json"
manifest = json.loads(manifest_path.read_text())
root = Path(__file__).resolve().parents[1]
for name, sha in manifest["source_sha256"].items():
if digest(root / name) != sha:
raise ValueError("Production source differs from exported run")
for source in manifest["adapters"]:
if digest(source["path"]) != source["sha256"]:
raise ValueError("Input adapter identity changed")
if "alpha" in source["metadata"]:
raise ValueError("File-level alpha requires separate native-loader policy validation")
exported_path = Path(manifest["export"])
if exported_path.stat().st_size != manifest["export_size"]:
raise ValueError("Export size changed")
meta = header(exported_path)["__metadata__"]
mode = effective_mode(meta)
cfg = json.loads(meta.get("merge_experimental", "null"))
if mode not in ("weighted_sum", "slerp", "per_prefix", "np_lora", "ct_merge"):
raise ValueError("This reference supports additive/SLERP/plain recorded per-prefix/NP/CT only")
if (mode in ("weighted_sum", "slerp", "per_prefix")) != (cfg is None):
raise ValueError("Export mode/config mismatch")
decisions, model_scale = {}, 1.
if mode == "per_prefix":
if digest(manifest["tuner_data"]) != manifest["tuner_data_sha256"]:
raise ValueError("Tuner evidence changed")
decisions, model_scale = replay_policy(manifest, meta)
sys.path.insert(0, str(comfy))
sys.argv = [sys.argv[0], "--cpu"]
import comfy.options
comfy.options.enable_args_parsing()
import torch
import comfy.lora
torch.set_num_threads(4)
torch.set_float32_matmul_precision("highest")
if not torch.cuda.is_available():
raise RuntimeError("Use the authorized GPU environment for the full-size check")
spec = importlib.util.spec_from_file_location("h3_native_reference", root / "experimental_merge.py")
exp = importlib.util.module_from_spec(spec)
spec.loader.exec_module(exp)
stable_node = None
if mode in ("slerp", "per_prefix"):
spec = importlib.util.spec_from_file_location("h3_slerp_reference", root / "lora_optimizer.py")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
stable_node = module.LoRAOptimizer()
base = header(manifest["checkpoint"])
state = {"diffusion_model." + k: torch.empty(v["shape"], device="meta")
for k, v in base.items() if k != "__metadata__" and k.endswith(".weight")}
shape_model = types.SimpleNamespace(state_dict=lambda: state, model_config=types.SimpleNamespace(unet_config={}))
mapping = comfy.lora.model_lora_keys_unet(shape_model, {})
sources = [load_native(a["path"], mapping, torch, comfy.lora) for a in manifest["adapters"]]
exported = load_native(exported_path, mapping, torch, comfy.lora)
require_coverage(sources, exported)
if decisions and set(decisions) != {
component_label(target, c) for target in exported
for c, _ in enumerate(component_rows(target, state[target].shape))}:
raise ValueError("Recorded decisions do not cover exactly the native component union")
results = []
started = time.monotonic()
for target in sorted(exported):
if not isinstance(target, str) or target not in state:
raise ValueError("Unexpected non-native or sliced target")
for component, rows in enumerate(component_rows(target, state[target].shape)):
local_mode = decisions[component_label(target, component)] if decisions else mode
up, down = factor_pair(exported[target], rows, "cuda")
actual = up @ down
if actual.shape != (rows.stop - rows.start, state[target].shape[1]):
raise ValueError("Export factor output differs from checkpoint target shape")
diffs, factors, indices = [], {}, []
for i, (source, weight) in enumerate(zip(sources, manifest["strengths"])):
if target not in source:
continue
b, a = factor_pair(source[target], rows, "cuda")
diffs.append((b @ a, weight * model_scale))
factors[i] = (b * weight, a)
indices.append(i)
additive = sum(d * w for d, w in diffs)
if stable_node is not None:
# Native mapping is independent; the pinned normal SLERP
# formula is shared with production, not a second algorithm.
expected = stable_node._merge_diffs(list(diffs), local_mode,
compute_device=torch.device("cuda"), keep_on_gpu=True)
storage_dtype = None
for i in indices:
weights = sources[i][target].weights
item_dtype = torch.promote_types(weights[0].dtype, weights[1].dtype)
storage_dtype = item_dtype if storage_dtype is None else torch.promote_types(storage_dtype, item_dtype)
rounded = expected.to(storage_dtype).float() if local_mode == "slerp" and len(indices) > 1 else expected
if not torch.isfinite(rounded).all():
rounded = expected
else:
expected = additive if cfg is None else exp.merge(diffs, mode, cfg,
source_indices=indices, role_indices=[0, 1], svd_factors=factors)
rounded = expected
error = (actual - expected).norm().item()
change = (expected - additive).norm().item()
results.append(dict(prefix=target, component=component,
local_mode=local_mode, model_scale=model_scale,
source_indices=indices, norm=expected.norm().item(),
relative_export_error=error / max(expected.norm().item(), 1e-30),
relative_normal_storage_rounding_error=(rounded - expected).norm().item() / max(expected.norm().item(), 1e-30),
relative_error_after_storage_rounding=(actual - rounded).norm().item() / max(rounded.norm().item(), 1e-30),
relative_method_change=change / max(additive.norm().item(), 1e-30),
error_to_method_change=error / change if change else None,
finite=bool(torch.isfinite(actual).all() and torch.isfinite(expected).all())))
del actual, expected, rounded, additive, diffs, factors, up, down, b, a
print(f"Checked native {target}", flush=True)
report = dict(mode=mode, export_metadata_mode=meta["merge_mode"], all_targets=True, native_loader="comfy.lora; no optimizer key normalization",
groups_checked=len(results), native_targets=len(exported),
source_native_targets=[len(s) for s in sources],
all_source_and_export_keys_consumed=True,
manifest_sha256=digest(manifest_path), export_sha256=digest(exported_path),
checker_sha256=digest(__file__), comfy_lora_sha256=digest(comfy.lora.__file__),
torch_version=torch.__version__, elapsed_seconds=time.monotonic()-started,
reference=("Pinned ordinary stable formulas and captured replay policy; FP32 discrepancy includes normal rounding/compression, reported separately"
if stable_node is not None else "FP32 additive or pinned experimental formula"),
results=results)
save_new(output, report)
print(json.dumps(dict(output=str(output), groups=len(results),
max_relative_export_error=max(r["relative_export_error"] for r in results),
finite=all(r["finite"] for r in results))))
if not all(r["finite"] for r in results):
raise ValueError("Non-finite export/reference; inspect preserved report")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--run", type=Path, required=True)
parser.add_argument("--comfy", type=Path, default=Path("/media/p5/Comfyui"))
args = parser.parse_args()
audit(args.run, args.comfy)