Fix garbage generation for Qwen3.5/Qwen3-Next under device_map CPU offload
Author: Sunt-ing
Pull request: yes

<!-- ci-dashboard-badge:start -->
[![CI](https://transformers-ci.lor-e.huggingface.cool/badge/pr?pr=46831)](https://transformers-ci.lor-e.huggingface.cool/d/pytest-observability-by-pr/pytest-observability-branch?var-pr=46831)
<!-- ci-dashboard-badge:end -->

# What does this PR do?

Hybrid GatedDeltaNet models (Qwen3.5, Qwen3-Next, Qwen3.5-MoE) generate garbage with `device_map="auto"` when some layers are offloaded to CPU, while the same model held entirely on one GPU is correct. A standard attention-only model is bit-identical under the same offload, so this is specific to the recurrent conv path, not to offloading in general. `device_map="auto"` is the usual way to run a model that does not fit on a single card, so the impact is broad.

Root cause: in the single-token decode path the GatedDeltaNet mixer reads `self.conv1d.weight` directly to feed `causal_conv1d_update`, without ever calling `self.conv1d.forward`. Under accelerate offload, a leaf module's weights are only moved onto the compute device by its own `AlignDevicesHook` when that module's `forward` runs. Prefill goes through `self.conv1d(...)`, so the hook fires and prefill is correct (logits match the single-GPU run exactly). Decode bypasses it, so `conv1d.weight` stays on the `meta` device and the convolution runs on weights with no real data, corrupting the output from the very first decoded token. The cached conv/recurrent states themselves are on the right device and bit-identical to the single-GPU run; only the conv weight is wrong.

Fix: before the per-step update reads `self.conv1d.weight`, fire conv1d's offload hook to materialize its weights onto the compute device. This mirrors how the codebase already handles offloaded weights that are touched outside a `forward` (for example gemma3n routes such access through `_hf_hook`). The change is in the modular sources for `qwen3_next` and `qwen3_5`; `qwen3_5_moe` inherits the forward and is regenerated. `olmo_hybrid` already does its decode conv inside a conv module's `forward`, so its hook fires and it is unaffected.

<details>
<summary>Reproduction and before/after (1x RTX 4090, transformers main, accelerate 1.14, fp16, no causal_conv1d installed)</summary>

```python
import torch
from transformers import AutoModelForCausalLM

def steps(mid, kw):
    m = AutoModelForCausalLM.from_pretrained(mid, dtype=torch.float16, **kw).eval()
    dev0 = next(p for p in m.parameters() if p.device.type != "meta").device
    ids = torch.arange(1, 17).unsqueeze(0).to(dev0)
    with torch.no_grad():
        g = m.generate(ids, max_new_tokens=8, do_sample=False,
                       output_scores=True, return_dict_in_generate=True)
    sc = [s.float().cpu() for s in g.scores]
    toks = g.sequences[0, ids.shape[1]:].cpu().tolist()
    del m; torch.cuda.empty_cache()
    return sc, toks

def run(mid, budget):
    print(f"===== {mid} =====")
    sf, tf = steps(mid, {"device_map": {"": 0}})                                     # full GPU
    so, to = steps(mid, {"device_map": "auto", "max_memory": {0: budget, "cpu": "12GiB"},
                         "offload_folder": "/tmp/off"})                              # offloaded
    print("  full tokens   :", tf)
    print("  offload tokens:", to)
    for i, (a, b) in enumerate(zip(sf, so)):
        d = (a - b).abs().max().item()
        print(f"  step {i}: max|dlogits|={d:.4f}{'  <-- DIVERGE' if d > 0.5 else ''}")

run("Qwen/Qwen3.5-0.8B", "700MiB")   # GatedDeltaNet hybrid
run("Qwen/Qwen3-0.6B", "700MiB")     # standard attention control
```

Before this PR:
```
===== Qwen/Qwen3.5-0.8B =====
  full tokens   : [17, 18, 19, 20, 21, 22, 23, 24]
  offload tokens: [17, 132251, 188955, 62, 11527, 113218, 233765, 94189]
  step 0: max|dlogits|=0.0000
  step 1: max|dlogits|=19.2065  <-- DIVERGE
  step 2..7: 17..24  <-- DIVERGE
===== Qwen/Qwen3-0.6B =====     # control, unaffected
  offload tokens: [17, 18, 19, 20, 21, 22, 23, 24]
  step 0..7: max|dlogits|=0.0000
```

After this PR:
```
===== Qwen/Qwen3.5-0.8B =====
  full tokens   : [17, 18, 19, 20, 21, 22, 23, 24]
  offload tokens: [17, 18, 19, 20, 21, 22, 23, 24]
  step 0..7: max|dlogits|=0.0000
===== Qwen/Qwen3-0.6B =====
  step 0..7: max|dlogits|=0.0000
```

Verified end-to-end on `Qwen/Qwen3.5-0.8B`. `qwen3_next` and `qwen3_5_moe` share the same modular-generated decode path and get the identical fix; no small public checkpoint was available to run them end-to-end.
</details>

## Code Agent Policy

- [x] I confirm that this is not a pure code agent PR.

## Before submitting

- [ ] This PR fixes a typo or improves the docs (you can dismiss the other checks if that's the case).
- [x] Did you read the [contributor guideline](https://huggingface.co/docs/transformers/contributing) and the [Pull Request](https://huggingface.co/docs/transformers/pr_checks) checks?
- [ ] Was this discussed/approved via a Github issue or the [forum](https://discuss.huggingface.co/)?
- [ ] Did you make sure to update the documentation with your changes?
- [ ] Did you write any new necessary [tests](https://huggingface.co/docs/transformers/testing)?

## Who can review?

@kashif @Cyrilvallez

<!-- ci-dashboard-recap:start -->

---

### CI recap

**Dashboard:** [View test results in Grafana](https://transformers-ci.lor-e.huggingface.cool/d/pytest-observability-by-pr/pytest-observability-branch?var-pr=46831)
**Latest run:** [28003359250:2](https://github.com/huggingface/transformers/actions/runs/28430343267)
**Result:** `success` | **Jobs:** 7 | **Tests:** 1,368 | **Failures:** 0 | **Duration:** 19s

<!-- ci-dashboard-recap:end -->

Comments:
**[For maintainers]** Suggested jobs to run (before merge)

run-slow: qwen3_5, qwen3_5_moe, qwen3_next

cc @sunmarc too maybe

Hey! Thanks for the issue! I made a much cleaner fix here https://github.com/huggingface/transformers/pull/46978, so closing this PR! Thanks again!

Thanks @Cyrilvallez, that makes sense. Moving the accelerate hook handling into a shared helper and applying it across the linear attention models is cleaner than keeping this scoped to the Qwen paths.