|
29 | 29 |
|
30 | 30 | from unsloth_zoo.temporary_patches.common import TEMPORARY_PATCHES # noqa: E402 |
31 | 31 | from unsloth_zoo.temporary_patches.qwen3_5_float32 import ( # noqa: E402 |
32 | | - _unsloth_fused_loss_kwargs, |
33 | | - _unsloth_is_default_causal_lm_loss, |
| 32 | + _unsloth_cast_position_embeddings, |
| 33 | + _unsloth_weight_dtype, |
34 | 34 | ) |
35 | 35 |
|
36 | 36 |
|
@@ -97,113 +97,41 @@ def test_qwen3_5_attention_dtype_mismatch_fixed(monkeypatch): |
97 | 97 | assert out.dtype == hidden.dtype |
98 | 98 |
|
99 | 99 |
|
100 | | -def test_qwen3_5_for_causal_lm_dtype_mismatch_fixed(monkeypatch): |
101 | | - """``Qwen3_5ForCausalLM`` must complete forward passes with fp16 weights.""" |
| 100 | +def test_qwen3_5_gated_delta_net_dtype_mismatch_fixed(monkeypatch): |
| 101 | + """``Qwen3_5GatedDeltaNet`` must accept bf16 activations when weights are fp16.""" |
102 | 102 | monkeypatch.setenv("UNSLOTH_FORCE_FLOAT32", "1") |
103 | 103 | _apply_temporary_patches() |
104 | 104 |
|
105 | | - config = _tiny_text_config(layer_types=("full_attention", "full_attention")) |
106 | | - model = qwen.Qwen3_5ForCausalLM(config).to(torch.float16) |
107 | | - input_ids = torch.randint(0, config.vocab_size, (2, 4)) |
108 | | - |
109 | | - outputs = model(input_ids, use_cache=False) |
110 | | - assert outputs.logits.shape == (2, 4, config.vocab_size) |
111 | | - |
112 | | - # The wrapper must preserve the standard Transformers tuple-output contract. |
113 | | - tuple_outputs = model(input_ids, use_cache=False, return_dict=False) |
114 | | - assert isinstance(tuple_outputs, tuple) |
115 | | - assert tuple_outputs[0].shape == (2, 4, config.vocab_size) |
116 | | - |
117 | | - |
118 | | -def test_qwen3_5_for_causal_lm_respects_output_hidden_states_config(monkeypatch): |
119 | | - """`config.output_hidden_states=True` must propagate even without the kwarg.""" |
120 | | - monkeypatch.setenv("UNSLOTH_FORCE_FLOAT32", "1") |
121 | | - _apply_temporary_patches() |
122 | | - |
123 | | - config = _tiny_text_config(layer_types=("full_attention", "full_attention")) |
124 | | - config.output_hidden_states = True |
125 | | - model = qwen.Qwen3_5ForCausalLM(config).to(torch.float16) |
126 | | - input_ids = torch.randint(0, config.vocab_size, (2, 4)) |
127 | | - |
128 | | - outputs = model(input_ids, use_cache=False) |
129 | | - assert outputs.logits.shape == (2, 4, config.vocab_size) |
130 | | - assert outputs.hidden_states is not None |
131 | | - |
| 105 | + config = _tiny_text_config(layer_types=("linear_attention",)) |
| 106 | + block = qwen.Qwen3_5GatedDeltaNet(config, layer_idx=0).to(torch.float16) |
| 107 | + x = torch.randn(2, 4, config.hidden_size, dtype=torch.bfloat16) |
132 | 108 |
|
133 | | -def test_qwen3_5_for_causal_lm_custom_loss_does_not_receive_return_dict(monkeypatch): |
134 | | - """A custom loss must not see the wrapper's synthetic return_dict kwarg.""" |
135 | | - monkeypatch.setenv("UNSLOTH_FORCE_FLOAT32", "1") |
136 | | - _apply_temporary_patches() |
| 109 | + out = block(x) |
| 110 | + assert out.shape == x.shape |
| 111 | + assert out.dtype == x.dtype |
137 | 112 |
|
138 | | - config = _tiny_text_config(layer_types=("full_attention", "full_attention")) |
139 | | - model = qwen.Qwen3_5ForCausalLM(config).to(torch.float16) |
140 | | - received_kwargs = {} |
141 | 113 |
|
142 | | - def custom_loss(*, logits, labels, vocab_size, **kwargs): |
143 | | - received_kwargs.update(kwargs) |
144 | | - return torch.tensor(0.0, dtype=logits.dtype) |
| 114 | +def test_unsloth_weight_dtype_skips_quantized_and_missing(): |
| 115 | + """`_unsloth_weight_dtype` must refuse quantized or absent weights.""" |
| 116 | + assert _unsloth_weight_dtype(None) is None |
145 | 117 |
|
146 | | - model.loss_function = custom_loss |
| 118 | + linear = torch.nn.Linear(4, 4) |
| 119 | + linear.weight = torch.nn.Parameter(torch.randn(4, 4, dtype=torch.float32)) |
| 120 | + assert _unsloth_weight_dtype(linear) is torch.float32 |
147 | 121 |
|
148 | | - input_ids = torch.randint(0, config.vocab_size, (2, 4)) |
149 | | - labels = input_ids.clone() |
150 | | - out = model(input_ids, labels=labels, use_cache=False) |
151 | | - assert out.loss is not None |
152 | | - assert "return_dict" not in received_kwargs |
| 122 | + # Fake quantized weight |
| 123 | + param = torch.nn.Parameter(torch.randn(4, 4, dtype=torch.float16)) |
| 124 | + param.quant_state = object() |
| 125 | + linear.weight = param |
| 126 | + assert _unsloth_weight_dtype(linear) is None |
153 | 127 |
|
154 | 128 |
|
155 | | -def test_qwen3_5_for_causal_lm_honors_accepts_loss_kwargs(monkeypatch): |
156 | | - """Respect accepts_loss_kwargs=False by not passing extra kwargs to loss.""" |
157 | | - monkeypatch.setenv("UNSLOTH_FORCE_FLOAT32", "1") |
158 | | - _apply_temporary_patches() |
| 129 | +def test_unsloth_cast_position_embeddings(): |
| 130 | + """`_unsloth_cast_position_embeddings` casts cos/sin to the target dtype.""" |
| 131 | + cos = torch.randn(1, 8, 16, dtype=torch.float32) |
| 132 | + sin = torch.randn(1, 8, 16, dtype=torch.float32) |
| 133 | + out = _unsloth_cast_position_embeddings((cos, sin), torch.float16) |
| 134 | + assert out[0].dtype is torch.float16 |
| 135 | + assert out[1].dtype is torch.float16 |
159 | 136 |
|
160 | | - config = _tiny_text_config(layer_types=("full_attention", "full_attention")) |
161 | | - model = qwen.Qwen3_5ForCausalLM(config).to(torch.float16) |
162 | | - received_kwargs = {} |
163 | | - |
164 | | - def custom_loss(*, logits, labels, vocab_size, **kwargs): |
165 | | - received_kwargs.update(kwargs) |
166 | | - return torch.tensor(0.0, dtype=logits.dtype) |
167 | | - |
168 | | - model.loss_function = custom_loss |
169 | | - model.accepts_loss_kwargs = False |
170 | | - |
171 | | - input_ids = torch.randint(0, config.vocab_size, (2, 4)) |
172 | | - labels = input_ids.clone() |
173 | | - out = model(input_ids, labels=labels, use_cache=False, num_items_in_batch=8) |
174 | | - assert out.loss is not None |
175 | | - assert "num_items_in_batch" not in received_kwargs |
176 | | - |
177 | | - |
178 | | -@pytest.mark.parametrize("name", ["ForCausalLMLoss", "UnslothForCausalLMLoss"]) |
179 | | -def test_default_causal_lm_loss_accepted(name): |
180 | | - """The default (and Unsloth-patched) loss names must take the fused path.""" |
181 | | - fake_loss = type("_FakeLoss", (), {"__name__": name})() |
182 | | - assert _unsloth_is_default_causal_lm_loss(fake_loss) is True |
183 | | - |
184 | | - |
185 | | -def test_custom_loss_rejected(): |
186 | | - """Custom losses must fall back to the logits + loss_function branch.""" |
187 | | - fake_loss = type("_FakeLoss", (), {"__name__": "CustomFocalLoss"})() |
188 | | - assert _unsloth_is_default_causal_lm_loss(fake_loss) is False |
189 | | - |
190 | | - |
191 | | -def test_fused_loss_kwargs_filter(): |
192 | | - """Model-only kwargs must not leak into the fused CE kernel.""" |
193 | | - kwargs = { |
194 | | - "num_items_in_batch": 8, |
195 | | - "shift_labels": False, |
196 | | - "ignore_index": -100, |
197 | | - "label_smoothing": 0.1, |
198 | | - "output_attentions": True, |
199 | | - "output_hidden_states": True, |
200 | | - "return_dict": True, |
201 | | - "use_cache": True, |
202 | | - } |
203 | | - filtered = _unsloth_fused_loss_kwargs(kwargs) |
204 | | - assert filtered == { |
205 | | - "num_items_in_batch": 8, |
206 | | - "shift_labels": False, |
207 | | - "ignore_index": -100, |
208 | | - "label_smoothing": 0.1, |
209 | | - } |
| 137 | + assert _unsloth_cast_position_embeddings(None, torch.float16) is None |
0 commit comments