fix(masking): preserve dict return type in create_masks_for_generate single-pattern path
Author: fyhon
Pull request: yes

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

## Summary

Fixes #46962. Restores the dict return-type contract of `create_masks_for_generate` for the single-pattern early-return path introduced by #41251, so that downstream models like `Qwen3Model.forward` do not silently rebuild the mask that `generate` just precomputed.

## Changes

- `src/transformers/masking_utils.py` (2 insertions, 1 deletion): wrap the single-pattern early-return in a dict keyed by the layer pattern name.

Before:

```python
if len(layer_patterns) == 1:
    return LAYER_PATTERN_TO_MASK_FUNCTION_MAPPING[next(iter(layer_patterns))](**mask_kwargs)
```

After:

```python
if len(layer_patterns) == 1:
    single_pattern = next(iter(layer_patterns))
    return {single_pattern: LAYER_PATTERN_TO_MASK_FUNCTION_MAPPING[single_pattern](**mask_kwargs)}
```

The unknown-types path (L1560-1561, `return attention_mask`) is intentionally left untouched: returning the raw attention_mask there is a deliberate 'defer to model' fallback whose semantics differ from the dict path — wrapping it in a synthetic key would over-reach.

## Root Cause

PR #41251 added two early-return paths in `create_masks_for_generate` that return bare values instead of the dict wrapper the function had always produced when `layer_types` was set. Downstream forward methods (e.g. `Qwen3Model.forward`, L403) detect precomputed masks with `isinstance(attention_mask, dict)`; a bare mask value fails the check and triggers a second `create_causal_mask(...)` call. In XPU/CPU paths this second call falls into `_can_skip_causal_mask` and cannot prove the mask is purely causal, so it builds a full 4D mask with `is_causal=False`, silently degrading generate() throughput.

## Duplicate Check

- `gh pr list --repo huggingface/transformers --state all --search "46962 in:body"` → 0 results
- `gh pr list --repo huggingface/transformers --state open --search "create_masks_for_generate return type"` → 0 results

## Testing

Environment: CPU-only container based on `hub.bilibili.co/nyx-base/vllm:0.18.0` (python 3.10, torch 2.10+cu128 with CUDA unavailable), transformers 5.8.0.dev0 (upstream/main@181beb3ba4 + this patch).

```
pytest tests/utils/test_masking_utils.py -q
# → 10 passed in 18.33s (up from 9 passed before PR #46738 landed)

pytest tests/models/qwen3/test_modeling_qwen3.py -q -k "mask or generate or forward or attention"
# → 58 passed, 22 skipped, 185 deselected in 28.40s

python3 repro_46962.py
# → single_pattern → dict (fixed), multi_pattern → dict (unchanged), unknown_types → bare (preserved by design)
```

Reproduction script (based on the `repro_contract.py` attached to the issue):

- Case A (`layer_types=["full_attention"]*4`) — used to return `Tensor`, now returns `{"full_attention": Tensor}` ✅
- Case B (`layer_types=["unknown_attn_type"]*2`) — still returns bare `None`, deliberate 'defer to model'
- Case C (`layer_types=["full_attention", "sliding_attention", "full_attention"]`) — always returned dict, unchanged

Downstream `Qwen3Model.forward` now uses the precomputed mask on Case A instead of rebuilding it.

## AI Disclosure

This PR was drafted with AI assistance (Claude Opus 4.6). The human submitter (@fyhon) has reviewed each changed line, ran the tests reported above, and takes responsibility for the change. The AI's role was scoped to:

1. Locating the two early-return paths and identifying which downstream models rely on the dict contract.
2. Composing the reproduction script and the patch.
3. Drafting this PR description.

No maintainer coordination was needed beyond the routing already present in the issue thread (Rocketknight1 cc'd @arthurzucker), because the fix is a direct restoration of the pre-#41251 return-type contract on the specific path that regresses Qwen3.

Comments:
<!-- 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=46988)
**Latest run:** [28489555808:2](https://github.com/huggingface/transformers/actions/runs/28489555808)
**Result:** `failure` | **Jobs:** 14 | **Tests:** 70,150 | **Failures:** 4 | **Duration:** 17h 55m

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

Would prefer to wait for Arthur to reply in the thread rather than just asking Claude to make a change immediately!