Fix SFTTrainer chunked-CE patch crash when forward is not a bound method#6539
Fix SFTTrainer chunked-CE patch crash when forward is not a bound method#6539verma8076 wants to merge 1 commit into
Conversation
`_patch_chunked_ce_lm_head` assumed `model.forward` is always a bound method and read `original_forward.__func__` to build the patched forward's signature. That assumption breaks for any model whose forward has been wrapped by accelerate's device-dispatch hooks (e.g. via `device_map="auto"`), since `add_hook_to_module` replaces `forward` with a `functools.partial`, which has no `__func__`. Use `inspect.unwrap(original_forward)` instead: it follows the `__wrapped__` chain set by both accelerate's partial (via `functools.update_wrapper`) and transformers' own `@wraps`-decorated forwards, landing on the correct unbound signature in both cases. Verified this produces an identical signature to the old code on an unwrapped model. Fixes huggingface#6483
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 571c9cb. Configure here.
| # too. Both set `__wrapped__`, and both make the bound method's implicit `self`-stripping not apply | ||
| # (a `functools.partial`/`@wraps`-wrapped function has no `__func__`). `inspect.unwrap` follows the | ||
| # full `__wrapped__` chain in either case, landing on the innermost unbound function with `self` intact. | ||
| _chunked_ce_forward.__signature__ = inspect.signature(inspect.unwrap(original_forward)) |
There was a problem hiding this comment.
Unwrap drops self from signature
Medium Severity
inspect.unwrap(original_forward) does not always land on an unbound function with self. For a plain bound method with no __wrapped__ chain, unwrap returns the bound method, so inspect.signature already omits self. After MethodType binding, introspection strips the first real parameter, breaking the signature that generate's _validate_model_kwargs relies on. The old .__func__ path preserved self in that case.
Reviewed by Cursor Bugbot for commit 571c9cb. Configure here.


What does this PR do?
Fixes the crash in
_patch_chunked_ce_lm_headreported in #6483. The issue title says it's a Qwen3.5 problem, but it's actually broader: the patch assumesmodel.forwardis always a bound method and readsoriginal_forward.__func__to build the patched forward's signature. That breaks for any model whose forward has been wrapped by accelerate's device-dispatch hooks — which is exactly whatdevice_map="auto"does when accelerate decides to dispatch/offload the model.accelerate.hooks.add_hook_to_modulereplacesmodel.forwardwith:A
functools.partialhas no__func__, hence theAttributeErrorin the issue.Fix: use
inspect.unwrap(original_forward)instead oforiginal_forward.__func__.functools.update_wrapperalways sets__wrapped__on the wrapper, pointing back atold_forward, soinspect.unwraprecovers the original callable regardless of whether it went through an accelerate hook. It also happens to correctly handle another layer of wrapping that's unrelated to this bug: sometransformersforwards are themselves decorated with@functools.wraps(e.g. viacan_return_tuple), which also sets__wrapped__. I checked thatinspect.unwrapproduces a byte-identical signature to the old.__func__access on a plain (non-hooked) model, so this isn't a behavior change for the common case — it just also works for the hooked one.I reproduced the crash locally against a small model wrapped with
accelerate.hooks.add_hook_to_module(no GPU or the actual Qwen3.5 checkpoint needed — the bug is in signature introspection, not model computation), confirmed the fix resolves it, and confirmed a real forward/backward pass through the patched model still produces correct results afterward.Before submitting
This wasn't pre-discussed with a maintainer — it's a small, self-contained bug fix (no public API or checkpoint format change) with a repro and regression test, so I went ahead and opened the PR directly rather than waiting on a design discussion. Happy to adjust if there's a reason to handle it differently.
AI writing disclosure
To be upfront about this: the investigation, fix, and test were done by Claude Code under my direction — I reviewed the reasoning and the diff, ran the tests myself, and I'm able to discuss/defend the change, but I want to flag the AI involvement honestly rather than undersell it.
Who can review?
@qgallouedec — you commented on the original issue and might have context on why the patch does the
.__func__dance in the first place.Note
Low Risk
Small, localized fix to signature copying during SFT chunked-CE patching, with a targeted regression test and no training API changes.
Overview
Fixes
AttributeErrorwhen_patch_chunked_ce_lm_headruns on models whoseforwardis not a plain bound method (e.g.device_map="auto"via Acceleratefunctools.partial, or Transformers@wrapslayers).Signature introspection now uses
inspect.signature(inspect.unwrap(original_forward))instead oforiginal_forward.__func__, so the patched forward keeps the real parameter list forgenerate/ VLMs without changing behavior on unwrapped models.Adds a CPU regression test that wraps
forwardwithAlignDevicesHook, asserts patching succeeds, and checks loss matches the reference model.Reviewed by Cursor Bugbot for commit 571c9cb. Bugbot is set up for automated code reviews on this repo. Configure here.