Skip to content

INT8 weights get re-quantized as plain tensorwise on LoRA offload (convrot, per-channel params dropped) #14642

Description

@JLuke73

Custom Node Testing

Expected Behavior

INT8 model + LoRA should produce the same quality regardless of --disable-dynamic-vram or whether layers offload - matching the clean output you get from an INT8 model with no LoRA.

What actually happens ("my journey", lol): INT8 model + LoRA + --disable-dynamic-vram = broken (low image quality in Ideogram4 with a normal LoRA loaded on both conditioned and unconditioned models). Removed the flag and it worked sometimes. INT8 model with no LoRA seems to work always. Tracing it, the degradation tracks with weights getting offloaded/re-quantized (the LoRA path), not the LoRA math itself.

I had GLM 5.2 take a crack at fixing it, but it couldn't. Then had Claude dig in, which found the re-quant calls drop the convrot/per_channel params (details below) and wrote a monkey-patch that avoids touching core code (also below, as an ugly init.py file). That restores quality and Opus claims its self-test shows ~6x lower re-quant error.

Actual Behavior

INT8 *_convrot_simple models degrade with LoRA because the re-quant calls drop the layout params.

Both sites in comfy/ops.py call:

QuantizedTensor.from_float(weight, "TensorWiseINT8Layout", scale="recalculate", ...)

without passing convrot / per_channel / convrot_groupsize, so TensorWiseINT8Layout.quantize defaults them to False/False/256 and a convrot + per-channel weight comes back as plain tensorwise INT8 (rotation gone, per-row scales collapsed to a scalar).

  • set_weight - resident LoRA path. Main "with LoRA -> degraded."
  • resolve_cast_module_with_vbar / post_cast - dynamic-VRAM re-quant (update_weight/want_requant); its orig.copy_(y) then broadcasts the scalar scale and flips convrot=False on the cached weight. Explains the no-LoRA "sometimes degraded" case.

Fix: inherit the original _params instead of defaulting. At both sites the original QuantizedTensor is in scope (self.weight / orig):

p = self.weight._params  # or orig._params in post_cast
QuantizedTensor.from_float(
    weight, self.layout_type,
    per_channel=(p.scale.numel() > 1),
    convrot=getattr(p, "convrot", False),
    convrot_groupsize=getattr(p, "convrot_groupsize", 256),
)

Cleaner still: a from_float/quantize path that recalculates only the scale and reuses the existing Params. And consider not round-tripping back to INT8 after a LoRA delta at all - the offloaded weight_function branch already keeps those layers in bf16.

Attached __init__.py is a monkey-patch that re-injects the params at both sites; restores quality, self-test shows ~6x lower re-quant error.

Env: ComfyUI @ 7cb784e, comfy-kitchen 0.2.12, torch 2.11.0+cu130.

Steps to Reproduce

Load a Ideogram 4 with any INT8 quant using the default (new) Comfy Load Diffusion Model node, and any LoRA with the normal Comfy Lora Loader node. Generate an image 512x512 (with enough VRAM) and it should look fine. Generate at 2048x2048 and it will break. Switch to a non-INT8 model in both cases, and the 512x512 will match the INT8 image, but the 2048x2048 will be similar in composition but with dramatically improved image quality.

Debug Logs

N/A

Other

init.py Monkey Patch written by Claude.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Potential BugUser is reporting a bug. This should be tested.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions