Bug 1 — Save after INT8 Grouped LoRA produces corrupted/unloadable checkpoint
Root cause: ModelPatcher.clone() constructs a fresh patcher and does not carry over arbitrary attributes that the loader had attached to the source patcher (_safetensors_metadata). After INT8GroupedLora.apply_loras() calls model.clone(), the metadata stash is gone, so INT8ModelSave writes a checkpoint with empty safetensors metadata.
Some model types (notably LTX2 — which keeps its transformer config in metadata) then fail to redetect on reload, and the file appears smaller because the metadata header is empty.
Fixes:
int8_unet_loader.py:225-235 — Stash metadata in two places: on the patcher (legacy) and on the inner model.model object (_int8_source_metadata). Inner-model attributes survive clone() because it shares the inner model by reference.
int8_lora.py:31-45 — Best-effort copy of _safetensors_metadata / _int8_source_metadata from the source patcher onto the cloned one in INT8GroupedLora.apply_loras.
int8_save.py:12-40 — New _resolve_source_metadata() that walks the patcher → parent chain and the inner-model fallback, plus a warning when nothing is found.
Bug 2 — Save node was collecting extra_keys BEFORE LoRA bake
comfy.sd.save_checkpoint internally triggers load_models_gpu, which is when the INT8ModelPatcher actually bakes LoRA patches into the int8 weights. The previous save node iterated modules before that, so on the LoRA-stacked path:
mark_module_for_direct_save was being called against the pre-bake module identity.
comfy_quant extra_keys were emitted against pre-bake state.
After save_checkpoint baked patches via set_attr(module, "weight", new_int8_param), the saved bytes were the patched int8 weights but with metadata that may or may not have observed the post-bake state cleanly.
Fix (int8_save.py:120-154): Pre-call comfy.model_management.load_models_gpu([model], force_full_load=True) so all LoRA patches are baked and comfy_patched_weights = True is set on every module before we walk it. Then re-finalize any aimdo-deferred layers, then collect extra_keys.
Bug 3 — OTF quantization round-trip was fragile
OTF-quantized layers were never stamping _convrot_groupsize on the module, so the save side fell back to the loader-time default (currently CONVROT_GROUP_SIZE = 256). It happened to match on reload, but any future change to the default would silently corrupt round-trips, and the saved comfy_quant JSON had no convrot_groupsize field at all.
Fixes:
int8_quant.py:254-257 and int8_quant.py:443-454 — Set self._convrot_groupsize = CONVROT_GROUP_SIZE whenever OTF (or the deferred aimdo finalize) actually applies a Hadamard rotation.
int8_save.py:166-187 — Always emit convrot_groupsize in comfy_quant whenever convrot=True, falling back to the module-level constant if _convrot_groupsize is somehow missing. Also emit per_row so the loader can pick the matching forward kernel without re-inspecting tensor shapes.
int8_quant.py:340-396 — Parse the new per_row hint in comfy_quant and use it to override the shape-based heuristic in _load_from_state_dict. Existing files (without per_row) continue to work via the original shape heuristic.
Files changed
int8_save.py — Restructured save flow, added metadata resolver, save convrot_groupsize and per_row deterministically, pre-load patches before extra_keys collection, robust fallback path.
int8_unet_loader.py:225-235 — Stash _int8_source_metadata on the inner model so it survives clone().
int8_lora.py:31-45 — Copy metadata attributes onto the cloned patcher in INT8GroupedLora.apply_loras.
int8_quant.py:240-256, 340-352, 371-401, 443-454 — Stamp _convrot_groupsize during OTF and aimdo-deferred finalize, parse per_row hint from comfy_quant.
Notes on the OTF "worse quality than pre-quantized" report
After re-tracing the OTF → save → reload path carefully (Hadamard rotation, per-row scale, bias preservation, dtype tracking) I was not able to find a save-side bug that would degrade quality.
The OTF and pre-quantized paths use the same quantize_int8_axiswise(weight, dim=1) code, the same forward kernels, and now write the same comfy_quant payload.
The most plausible source of the perceived quality gap is that pre-quantized HF checkpoints were calibrated with a different excluded_names list per model than what Int8TensorwiseOps.excluded_names uses for the same model in this loader; that is a calibration choice, not a bug in the save/reload path.
If a concrete reproduction (same workflow, same seed, OTF-then-save vs HF-prequant) shows a measurable delta, the fix would belong in the per-model excluded_names table in int8_unet_loader.py, not in the save node.