niqun123123 commited on
Commit
3ee1348
·
verified ·
1 Parent(s): 34322a7

Upload modeling_lemondecider.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. modeling_lemondecider.py +179 -0
modeling_lemondecider.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """LemonDecider full-weight decision model. No autoregressive generation head.
2
+
3
+ The checkpoint's backbone is copied byte-for-byte from the base LM, while readout
4
+ rows are selected from its original LM head. T is used in postprocessing, not CE loss.
5
+ """
6
+ from dataclasses import dataclass
7
+ from contextlib import nullcontext
8
+ import torch
9
+ import torch.nn.functional as F
10
+ from transformers.modeling_utils import PreTrainedModel
11
+ from transformers.utils import ModelOutput
12
+ from transformers.models.llama.modeling_llama import LlamaModel
13
+ from transformers.models.qwen3_5.modeling_qwen3_5 import Qwen3_5Model, Qwen3_5Attention
14
+ from .configuration_lemondecider import LemonDeciderConfig
15
+ from .option_loss import option_cross_entropy
16
+ from .contract import LemonDeciderReadout, enable_noncausal_full_attention, sdpa_backends, linearize_patch_embed
17
+
18
+
19
+ @dataclass
20
+ class LemonDeciderOutput(ModelOutput):
21
+ loss: torch.Tensor | None = None
22
+ logits: torch.Tensor | None = None
23
+
24
+
25
+ def final_norm_module(model):
26
+ """Last language RMSNorm feeding the decision readout."""
27
+ backbone = model.backbone if hasattr(model, "backbone") else model
28
+ language = getattr(backbone, "language_model", None)
29
+ if language is not None and hasattr(language, "norm"):
30
+ return language.norm
31
+ if hasattr(backbone, "norm"):
32
+ return backbone.norm
33
+ raise AttributeError("Backbone has no final RMSNorm")
34
+
35
+
36
+ class LemonDeciderForDecisionClassification(PreTrainedModel):
37
+ config_class = LemonDeciderConfig
38
+ base_model_prefix = "backbone"
39
+ supports_gradient_checkpointing = True
40
+ _no_split_modules = ["Qwen3_5DecoderLayer", "Qwen3_5VisionBlock", "LlamaDecoderLayer"]
41
+ _supports_flash_attn = True
42
+ _supports_flash_attn_2 = True
43
+ _supports_sdpa = True
44
+ _supports_cache_class = False
45
+
46
+ def __init__(self, config):
47
+ super().__init__(config)
48
+ hidden = config.text_config.hidden_size
49
+ attn = getattr(config, "_attn_implementation", None) or getattr(config, "_attn_implementation_internal", None)
50
+ attention_mode = getattr(config, "attention_mode", "causal")
51
+ if attention_mode not in {"causal", "noncausal_full_attention"}:
52
+ raise ValueError(f"Unknown attention mode: {attention_mode}")
53
+ if getattr(config, "pooling", "last") != "last":
54
+ raise ValueError(f"Unknown pooling mode: {getattr(config, 'pooling', 'last')}")
55
+ if getattr(config, "backbone_type", "qwen3_5") == "llama":
56
+ if attention_mode != "causal":
57
+ raise ValueError("noncausal_full_attention is only implemented for the Qwen3.5 hybrid backbone")
58
+ inner = config.llama_config
59
+ if attn:
60
+ inner._attn_implementation = attn
61
+ self.backbone = LlamaModel(inner)
62
+ else:
63
+ if attention_mode == "noncausal_full_attention":
64
+ config.text_config.is_causal = False
65
+ self.backbone = Qwen3_5Model(config)
66
+ self.backbone.language_model.config.is_causal = attention_mode == 'causal'
67
+ # SDPA may omit the all-valid mask. Qwen's module flag otherwise
68
+ # re-enables causality even when its config requested bidirectional.
69
+ # Restrict to full attention; never modify the linear recurrence.
70
+ for module in self.backbone.language_model.modules():
71
+ if isinstance(module, Qwen3_5Attention):
72
+ module.is_causal = attention_mode == 'causal'
73
+ decision = getattr(config, 'decision_protocol', 'autojev-v1') == 'lemondecider-v1'
74
+ if decision:
75
+ if linearize_patch_embed(self.backbone) != 1:
76
+ raise ValueError('LemonDecider requires exactly one matrix-product vision patch stem')
77
+ enable_noncausal_full_attention(self.backbone.language_model)
78
+ self._keep_in_fp32_modules_strict = ['readout']
79
+ self.readout = (LemonDeciderReadout if decision else torch.nn.Linear)(hidden, config.max_options, bias=False)
80
+ self.post_init()
81
+
82
+ def get_input_embeddings(self):
83
+ return self.backbone.get_input_embeddings()
84
+
85
+ def set_input_embeddings(self, value):
86
+ self.backbone.set_input_embeddings(value)
87
+
88
+ def gradient_checkpointing_enable(self, **kwargs):
89
+ super().gradient_checkpointing_enable(**kwargs)
90
+ if hasattr(self.backbone, "gradient_checkpointing_enable"):
91
+ self.backbone.gradient_checkpointing_enable(**kwargs)
92
+
93
+ def forward(self, input_ids=None, attention_mask=None, option_counts=None,
94
+ labels=None, return_dict=True, **kwargs):
95
+ if input_ids is None or input_ids.ndim != 2 or input_ids.shape[1] == 0:
96
+ raise ValueError("input_ids must be a nonempty [batch, length] tensor")
97
+ if input_ids.shape[1] > self.config.max_input_length:
98
+ raise ValueError("Input exceeds max_input_length; truncation is not allowed")
99
+ if attention_mask is None:
100
+ attention_mask = torch.ones_like(input_ids)
101
+ if attention_mask.shape != input_ids.shape or not bool((attention_mask[:, -1] == 1).all()):
102
+ raise ValueError("Left padding required: last prompt position must be valid")
103
+ if option_counts is None:
104
+ raise ValueError("option_counts is required for each input")
105
+ counts = torch.as_tensor(option_counts, device=input_ids.device)
106
+ if counts.dtype == torch.bool or counts.is_floating_point() or counts.shape != (input_ids.shape[0],):
107
+ raise ValueError("option_counts must contain one integer per row")
108
+ if not bool(((counts >= 1) & (counts <= self.config.max_options)).all()):
109
+ raise ValueError(f"Each question must contain 1..{self.config.max_options} options")
110
+ if kwargs.pop("use_cache", False) or kwargs.get("past_key_values") is not None:
111
+ raise ValueError("Decision forward does not accept KV-cache decoding")
112
+ if getattr(self.config, "attention_mode", "causal") == "noncausal_full_attention":
113
+ implementation = getattr(self.config, "_attn_implementation", None) or getattr(
114
+ self.config, "_attn_implementation_internal", None
115
+ )
116
+ if implementation != "sdpa":
117
+ raise ValueError("noncausal_full_attention requires SDPA")
118
+ language = getattr(self.backbone, "language_model", None)
119
+ if language is None or getattr(language.config, "is_causal", True):
120
+ raise ValueError("noncausal_full_attention requires language_model.config.is_causal=False")
121
+ vision_zero = None
122
+ if getattr(self.config, 'vision_training_mode', 'frozen') == 'all-trainable' and kwargs.get('pixel_values') is None:
123
+ # Execute the SAME vision FSDP units on image-free and dummy ranks.
124
+ # A minimal synthetic patch grid has zero loss mass, not a dataset row.
125
+ visual = self.backbone.visual
126
+ vc = self.config.vision_config
127
+ merge = vc.spatial_merge_size
128
+ pixels = torch.zeros(merge * merge, vc.in_channels * vc.temporal_patch_size * vc.patch_size ** 2,
129
+ device=input_ids.device, dtype=visual.dtype)
130
+ grid = torch.tensor([[1, merge, merge]], device=input_ids.device, dtype=torch.long)
131
+ vision_zero = visual(pixels, grid_thw=grid, return_dict=True).pooler_output.sum() * 0.0
132
+ backbone_ids = input_ids
133
+ if vision_zero is not None:
134
+ # Link BEFORE the language stack, like real image embeddings, so
135
+ # backward FSDP collective order also agrees across ranks.
136
+ embeds = self.backbone.get_input_embeddings()(input_ids)
137
+ kwargs['inputs_embeds'] = embeds + vision_zero.to(embeds.dtype)
138
+ if kwargs.get('position_ids') is None:
139
+ # Same text-only arange as QwenTextModel; avoid stale M-RoPE
140
+ # deltas from a preceding multimodal batch when passing embeds.
141
+ kwargs['position_ids'] = torch.arange(input_ids.shape[1], device=input_ids.device)[None, None].expand(3, input_ids.shape[0], -1)
142
+ backbone_ids = None
143
+ backend_context = (sdpa_backends(input_ids.device)
144
+ if getattr(self.config, 'decision_protocol', 'autojev-v1') == 'lemondecider-v1' else nullcontext())
145
+ with backend_context:
146
+ output = self.backbone(input_ids=backbone_ids, attention_mask=attention_mask,
147
+ use_cache=False, return_dict=True, **kwargs)
148
+ hidden = output.last_hidden_state[:, -1, :]
149
+ # A separate FSDP precision island keeps the readout weights and GEMM FP32.
150
+ # Passing FP32 also prevents any mixed-precision wrapper from downcasting logits.
151
+ logits = self.readout(hidden.float() if getattr(self.config, 'decision_protocol', 'autojev-v1') == 'lemondecider-v1' else hidden).float()
152
+ invalid = torch.arange(self.config.max_options, device=logits.device)[None] >= counts[:, None]
153
+ logits = logits.masked_fill(invalid, -1e9)
154
+ loss = None
155
+ if labels is not None:
156
+ labels = labels.to(logits.device)
157
+ if labels.shape == (len(counts),):
158
+ if labels.is_floating_point() or labels.dtype == torch.bool or not bool(((labels >= 0) & (labels < counts)).all()):
159
+ raise ValueError("Hard targets must be valid option indices")
160
+ target = F.one_hot(labels.long(), self.config.max_options).float()
161
+ loss = option_cross_entropy(logits, target, counts)
162
+ elif labels.shape == logits.shape:
163
+ target = labels.float()
164
+ if not bool(torch.isfinite(target).all()) or bool((target < 0).any()):
165
+ raise ValueError("Invalid soft targets")
166
+ if not torch.allclose(target.sum(-1), torch.ones(len(counts), device=target.device), atol=1e-6, rtol=0):
167
+ raise ValueError("Soft targets must sum to one")
168
+ if bool((target.masked_select(invalid) != 0).any()):
169
+ raise ValueError("Soft targets assign mass to invalid options")
170
+ loss = option_cross_entropy(logits, target, counts)
171
+ else:
172
+ raise ValueError(f"labels must be [batch] or [batch,{self.config.max_options}]")
173
+ if not return_dict:
174
+ return (loss, logits) if loss is not None else (logits,)
175
+ return LemonDeciderOutput(loss=loss, logits=logits)
176
+
177
+
178
+ LemonDeciderConfig.register_for_auto_class()
179
+ LemonDeciderForDecisionClassification.register_for_auto_class("AutoModel")