Skip to content

Commit 750b97a

Browse files
Narrow Qwen3.5 float32 patch to dtype-normalization wrappers
Remove the reimplemented Qwen3_5ForCausalLM and Qwen3_5ForConditionalGeneration forwards, the fused-loss helpers, and the vision-tower dtype wrappers. Keep only per-component dtype normalization for GatedDeltaNet, Attention and MLP, matching the qwen3_moe_float32.py component-level shape. Update regression tests to the narrower scope. Assisted-by: Claude Sonnet
1 parent ad958ac commit 750b97a

2 files changed

Lines changed: 45 additions & 458 deletions

File tree

tests/test_qwen3_5_float32.py

Lines changed: 29 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929

3030
from unsloth_zoo.temporary_patches.common import TEMPORARY_PATCHES # noqa: E402
3131
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,
3434
)
3535

3636

@@ -97,113 +97,41 @@ def test_qwen3_5_attention_dtype_mismatch_fixed(monkeypatch):
9797
assert out.dtype == hidden.dtype
9898

9999

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."""
102102
monkeypatch.setenv("UNSLOTH_FORCE_FLOAT32", "1")
103103
_apply_temporary_patches()
104104

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)
132108

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
137112

138-
config = _tiny_text_config(layer_types=("full_attention", "full_attention"))
139-
model = qwen.Qwen3_5ForCausalLM(config).to(torch.float16)
140-
received_kwargs = {}
141113

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
145117

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
147121

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
153127

154128

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
159136

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

Comments
 (0)