File size: 17,976 Bytes
822adec
 
 
 
059efd9
 
 
 
 
822adec
 
059efd9
 
822adec
 
059efd9
 
 
 
 
 
 
822adec
 
 
 
059efd9
 
822adec
 
 
 
 
 
 
 
 
 
059efd9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
822adec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
059efd9
822adec
059efd9
822adec
 
 
 
 
 
059efd9
 
 
 
822adec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
059efd9
 
 
 
 
 
822adec
 
 
 
 
059efd9
 
 
 
 
 
 
 
 
 
822adec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
059efd9
 
 
 
 
822adec
059efd9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
822adec
 
059efd9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
822adec
 
 
059efd9
 
822adec
059efd9
 
 
 
 
 
 
 
 
 
 
 
 
 
822adec
 
059efd9
822adec
 
 
 
 
059efd9
822adec
 
059efd9
 
 
 
 
 
 
 
822adec
 
 
059efd9
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
"""Matilda Platypus: lower block of a larger Qwen3 parent -> Representation Bridge -> upper block of a smaller parent.

Self-contained: the two blocks are ordinary Hugging Face Qwen3 checkpoints run through their public forward passes
(the lower one truncated to its first k layers with its final norm removed, the upper one starting at layer j with
`inputs_embeds`), so no custom kernels are involved. `generate` keeps one KV cache per block and follows the standard
`transformers` keyword arguments (`max_new_tokens` / `max_length`, `stopping_criteria`, `logits_processor`, sampling
knobs, `eos_token_id` / `pad_token_id`, `streamer`), so it runs unchanged under evaluation harnesses such as
lm-eval-harness (`--model hf --model_args pretrained=...,trust_remote_code=True`). Greedy and sampling only; beam
search is not implemented and raises.

    from transformers import AutoModelForCausalLM, AutoTokenizer
    tok = AutoTokenizer.from_pretrained("Maincode/Matilda-Platypus-7B-Base")
    model = AutoModelForCausalLM.from_pretrained("Maincode/Matilda-Platypus-7B-Base", trust_remote_code=True,
                                                 dtype=torch.bfloat16).cuda()
    out = model.generate(**tok("The capital of Australia is", return_tensors="pt").to("cuda"), max_new_tokens=32)

Remote-code revision 2 (2026-10-31): `generate` honours `max_length`, `stopping_criteria`, `logits_processor`,
`min_new_tokens`, `top_k`, `repetition_penalty`, list-valued `eos_token_id`, `generation_config` and `streamer`, and
warns instead of silently ignoring unknown arguments; `tie_weights()` is a no-op at the top level (the lower block's
4096-d embedding and the upper block's 2560-d head must never be tied, and each parent was already tied on load).
Revision 1 ignored `max_length` and `stopping_criteria` (it always produced its default of 128 new tokens), which
truncated chain-of-thought answers under lm-eval and under-reported generative benchmarks.
"""
from __future__ import annotations

import os
import warnings
from typing import Optional, Sequence, Tuple, Union

import torch
import torch.nn as nn
from safetensors.torch import load_file
from transformers import AutoModelForCausalLM, PreTrainedModel
from transformers.cache_utils import DynamicCache
from transformers.modeling_outputs import CausalLMOutputWithPast

from .configuration_platypus import MatildaPlatypusConfig

REMOTE_CODE_REVISION = 2
_DEFAULT_NEW_TOKENS = 128  # used only when neither max_new_tokens, max_length nor a generation_config gives a budget

# generate() arguments that would change the result if ignored -> refuse loudly instead of pretending
_UNSUPPORTED_GENERATE_KWARGS = ("prefix_allowed_tokens_fn", "bad_words_ids", "force_words_ids", "suppress_tokens",
                                "begin_suppress_tokens", "forced_bos_token_id", "forced_eos_token_id", "constraints",
                                "assistant_model", "guidance_scale", "penalty_alpha", "num_beam_groups",
                                "diversity_penalty", "exponential_decay_length_penalty", "sequence_bias",
                                "no_repeat_ngram_size", "encoder_repetition_penalty", "typical_p", "epsilon_cutoff",
                                "eta_cutoff", "renormalize_logits", "token_healing", "dola_layers", "watermarking_config")
# generate() arguments that are safe to accept and ignore
_IGNORED_GENERATE_KWARGS = ("use_cache", "synced_gpus", "output_attentions", "output_hidden_states", "output_scores",
                            "output_logits", "return_legacy_cache", "cache_implementation", "cache_config",
                            "compile_config", "disable_compile", "negative_prompt_ids", "negative_prompt_attention_mask",
                            "kwargs", "tokenizer", "position_ids", "cache_position", "token_type_ids")


class RepresentationBridge(nn.Module):
    """y = W x + b on the residual stream, computed in float32 (W: [d_upper, d_lower])."""

    def __init__(self, d_lower: int, d_upper: int):
        super().__init__()
        self.proj = nn.Linear(d_lower, d_upper, bias=True)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        with torch.autocast(device_type=x.device.type, enabled=False):
            return self.proj(x.float())


class MatildaPlatypusForCausalLM(PreTrainedModel):
    config_class = MatildaPlatypusConfig
    main_input_name = "input_ids"
    _supports_cache_class = True
    _tied_weights_keys = None  # nothing is tied across the two blocks (see tie_weights)

    def __init__(self, config: MatildaPlatypusConfig, lower: Optional[PreTrainedModel] = None,
                 upper: Optional[PreTrainedModel] = None, bridge: Optional[RepresentationBridge] = None):
        super().__init__(config)
        self.lower, self.upper, self.bridge = lower, upper, bridge

    @classmethod
    def can_generate(cls) -> bool:  # text-generation pipelines ask this before calling .generate()
        return True

    # ------------------------------------------------------------------ loading
    @classmethod
    def from_pretrained(cls, pretrained_model_name_or_path, *args, dtype=None, torch_dtype=None, token=None,
                        revision=None, device_map=None, **kwargs):
        from huggingface_hub import snapshot_download
        path = str(pretrained_model_name_or_path)
        local = path if os.path.isdir(path) else snapshot_download(path, token=token, revision=revision)
        config = MatildaPlatypusConfig.from_pretrained(local)
        dt = dtype or torch_dtype or torch.bfloat16
        lower_src = os.path.join(local, config.lower_model) if os.path.isdir(os.path.join(local, config.lower_model)) else config.lower_model
        lower = AutoModelForCausalLM.from_pretrained(lower_src, dtype=dt, token=token)
        upper = AutoModelForCausalLM.from_pretrained(os.path.join(local, config.upper_model), dtype=dt)
        k, j, m = config.lower_layers, config.upper_start_layer, config.upper_layers
        # lower block: first k decoder layers, no final norm (the bridge takes the raw residual stream)
        lower.model.layers = nn.ModuleList(list(lower.model.layers)[:k])
        lower.model.norm = nn.Identity()
        lower.config.num_hidden_layers = k
        if getattr(lower.config, "layer_types", None):
            lower.config.layer_types = list(lower.config.layer_types)[:k]
        lower.lm_head = nn.Identity()
        # upper block: layers j.. of the smaller parent, re-indexed so a fresh KV cache is contiguous
        layers = list(upper.model.layers)[j:j + m]
        for n, layer in enumerate(layers):
            if hasattr(layer, "self_attn"):
                layer.self_attn.layer_idx = n
            if hasattr(layer, "layer_idx"):
                layer.layer_idx = n
        upper.model.layers = nn.ModuleList(layers)
        upper.config.num_hidden_layers = m
        if getattr(upper.config, "layer_types", None):
            upper.config.layer_types = list(upper.config.layer_types)[j:j + m]
        upper.model.embed_tokens = nn.Identity()  # the upper block never embeds tokens
        bridge = RepresentationBridge(config.lower_hidden_size, config.upper_hidden_size)
        t = load_file(os.path.join(local, config.bridge_file))
        bridge.proj.weight.data.copy_(t["W"].float()); bridge.proj.bias.data.copy_(t["b"].float())
        for p in lower.parameters():
            p.requires_grad_(False)
        # context length = the shorter of the two parents (harnesses read this to size their prompts)
        mp = [getattr(c, "max_position_embeddings", None) for c in (lower.config, upper.config)]
        mp = [x for x in mp if isinstance(x, int)]
        if mp and getattr(config, "max_position_embeddings", None) is None:
            config.max_position_embeddings = min(mp)
        config.tie_word_embeddings = False
        model = cls(config, lower, upper, bridge).eval()
        if device_map is not None and isinstance(device_map, (str, torch.device)) and device_map != "auto":
            model.to(device_map)
        return model

    # ------------------------------------------------------------------ weight tying: never across blocks
    def tie_weights(self, *args, **kwargs) -> None:
        """No-op. The lower block's input embedding (d_lower) and the upper block's output head (d_upper) have
        different widths and must never be tied; each parent block was already tied (if its config says so) when
        it was loaded, and the upper block's token embedding has since been replaced, so re-tying would fail."""
        return None

    def tie_embeddings_and_encoder_decoder(self) -> None:  # transformers 4.x entry point
        return None

    # ------------------------------------------------------------------ forward
    @property
    def dtype(self) -> torch.dtype:
        return self.upper.lm_head.weight.dtype

    def get_input_embeddings(self):
        return self.lower.model.embed_tokens

    def get_output_embeddings(self):
        return self.upper.lm_head

    def forward(self, input_ids: torch.LongTensor, attention_mask: Optional[torch.Tensor] = None,
                past_key_values: Optional[Tuple[DynamicCache, DynamicCache]] = None, use_cache: bool = False,
                **kwargs) -> CausalLMOutputWithPast:
        cl, cu = past_key_values if past_key_values is not None else (None, None)
        if use_cache and cl is None:
            cl, cu = DynamicCache(), DynamicCache()
        lo = self.lower.model(input_ids=input_ids, attention_mask=attention_mask, past_key_values=cl, use_cache=use_cache)
        h = self.bridge(lo.last_hidden_state).to(self.dtype)
        up = self.upper.model(inputs_embeds=h, attention_mask=attention_mask, past_key_values=cu, use_cache=use_cache)
        logits = self.upper.lm_head(up.last_hidden_state)
        return CausalLMOutputWithPast(logits=logits, past_key_values=(cl, cu) if use_cache else None)

    # ------------------------------------------------------------------ generation
    def _default_eos(self) -> Optional[Union[int, Sequence[int]]]:
        gc = getattr(self.upper, "generation_config", None)
        eos = getattr(gc, "eos_token_id", None) if gc is not None else None
        return eos if eos is not None else getattr(self.upper.config, "eos_token_id", None)

    @torch.no_grad()
    def generate(self, input_ids: Optional[torch.LongTensor] = None, attention_mask: Optional[torch.Tensor] = None, *,
                 inputs: Optional[torch.LongTensor] = None, generation_config=None,
                 max_new_tokens: Optional[int] = None, max_length: Optional[int] = None,
                 min_new_tokens: Optional[int] = None, do_sample: Optional[bool] = None,
                 temperature: Optional[float] = None, top_k: Optional[int] = None, top_p: Optional[float] = None,
                 repetition_penalty: Optional[float] = None,
                 eos_token_id: Optional[Union[int, Sequence[int]]] = None, pad_token_id: Optional[int] = None,
                 stopping_criteria=None, logits_processor=None, streamer=None,
                 num_beams: int = 1, num_return_sequences: int = 1, return_dict_in_generate: bool = False,
                 **kwargs) -> torch.LongTensor:
        """Greedy or sampled decoding with one KV cache per block, following the `transformers.generate` contract.

        Budget: `max_new_tokens` > `max_length` (total length, prompt included) > `generation_config.max_new_tokens`
        > 128. A row stops at any `eos_token_id` (int or list), when `stopping_criteria` says so, or at the budget;
        finished rows are padded with `pad_token_id`. `logits_processor` (a `LogitsProcessorList`) is applied before
        sampling. Beam search and `num_return_sequences > 1` are not implemented.
        """
        if input_ids is None:
            input_ids = inputs
        if input_ids is None:
            raise ValueError("generate() needs `input_ids` (or `inputs`)")
        if num_beams != 1 or num_return_sequences != 1:
            raise NotImplementedError("Matilda Platypus implements greedy and sampled decoding only "
                                      "(num_beams=1, num_return_sequences=1)")
        bad = [k for k in _UNSUPPORTED_GENERATE_KWARGS if kwargs.get(k) not in (None, False, [], 1.0, 0)]
        if bad:
            raise NotImplementedError(f"generate() arguments not supported by Matilda Platypus: {bad}")
        unknown = sorted(k for k in kwargs if k not in _UNSUPPORTED_GENERATE_KWARGS and k not in _IGNORED_GENERATE_KWARGS)
        if unknown:
            warnings.warn(f"MatildaPlatypusForCausalLM.generate() is ignoring unrecognised arguments: {unknown}",
                          stacklevel=2)
        gc = generation_config

        def pick(name, value, default):
            if value is not None:
                return value
            v = getattr(gc, name, None) if gc is not None else None
            return default if v is None else v

        device = self.device
        input_ids = input_ids.to(device)
        batch, prompt_len = input_ids.shape
        if attention_mask is None:
            attention_mask = torch.ones_like(input_ids)
        attention_mask = attention_mask.to(device)

        # ---- budget
        if max_new_tokens is None and max_length is not None:
            budget = int(max_length) - prompt_len
        else:
            if max_new_tokens is not None and max_length is not None:
                warnings.warn("Both `max_new_tokens` and `max_length` were given; `max_new_tokens` takes precedence.",
                              stacklevel=2)
            budget = pick("max_new_tokens", max_new_tokens, None)
            if budget is None:
                budget = _DEFAULT_NEW_TOKENS
        budget = int(budget)
        if budget <= 0:
            warnings.warn(f"generate(): the prompt ({prompt_len} tokens) already fills the requested length; "
                          "returning the prompt unchanged.", stacklevel=2)
            return self._wrap(input_ids, return_dict_in_generate)
        min_new = int(pick("min_new_tokens", min_new_tokens, 0) or 0)

        # ---- special tokens
        eos = pick("eos_token_id", eos_token_id, None)
        if eos is None:
            eos = self._default_eos()
        eos_list = [int(e) for e in (eos if isinstance(eos, (list, tuple)) else ([] if eos is None else [eos]))]
        eos_t = torch.tensor(eos_list, device=device, dtype=input_ids.dtype) if eos_list else None
        pad = pick("pad_token_id", pad_token_id, None)
        if pad is None:
            pad = getattr(self.upper.config, "pad_token_id", None)
        if pad is None:
            pad = eos_list[0] if eos_list else 0
        pad = int(pad)

        # ---- sampling knobs (greedy unless do_sample)
        sample = bool(pick("do_sample", do_sample, False))
        temp = float(pick("temperature", temperature, 1.0))
        tk = pick("top_k", top_k, None)
        tp = float(pick("top_p", top_p, 1.0))
        rep = float(pick("repetition_penalty", repetition_penalty, 1.0))

        if streamer is not None and batch == 1:
            streamer.put(input_ids.cpu())
        out = self.forward(input_ids, attention_mask=attention_mask, use_cache=True)
        caches = out.past_key_values
        seq = input_ids
        done = torch.zeros(batch, dtype=torch.bool, device=device)
        for step in range(budget):
            logits = out.logits[:, -1, :].float()
            if rep != 1.0:
                prev = torch.gather(logits, 1, seq)
                prev = torch.where(prev < 0, prev * rep, prev / rep)
                logits = logits.scatter(1, seq, prev)
            if logits_processor is not None:
                logits = logits_processor(seq, logits)
            if step < min_new and eos_t is not None:
                logits[:, eos_t] = float("-inf")
            if sample:
                logits = logits / max(temp, 1e-5)
                if tk is not None and 0 < int(tk) < logits.shape[-1]:
                    kth = torch.topk(logits, int(tk), dim=-1).values[:, -1, None]
                    logits = logits.masked_fill(logits < kth, float("-inf"))
                if tp < 1.0:
                    sorted_logits, idx = torch.sort(logits, descending=True)
                    probs = torch.softmax(sorted_logits, dim=-1)
                    keep = probs.cumsum(-1) - probs < tp
                    sorted_logits[~keep] = float("-inf")
                    logits = torch.full_like(logits, float("-inf")).scatter(1, idx, sorted_logits)
                nxt = torch.multinomial(torch.softmax(logits, dim=-1), 1).squeeze(1)
            else:
                nxt = logits.argmax(-1)
            nxt = torch.where(done, torch.full_like(nxt, pad), nxt)
            seq = torch.cat([seq, nxt[:, None]], dim=1)
            attention_mask = torch.cat([attention_mask, torch.ones_like(nxt[:, None])], dim=1)
            if streamer is not None and batch == 1:
                streamer.put(nxt.cpu())
            if eos_t is not None:
                done |= torch.isin(nxt, eos_t)
            if stopping_criteria is not None:
                flag = stopping_criteria(seq, logits)
                flag = torch.as_tensor(flag, device=device).to(torch.bool)
                done |= flag if flag.dim() else flag.expand(batch)
            if bool(done.all()):
                break
            out = self.forward(nxt[:, None], attention_mask=attention_mask, past_key_values=caches, use_cache=True)
        if streamer is not None:
            streamer.end()
        return self._wrap(seq, return_dict_in_generate)

    @staticmethod
    def _wrap(seq: torch.LongTensor, return_dict: bool):
        if not return_dict:
            return seq
        try:
            from transformers.generation.utils import GenerateDecoderOnlyOutput
            return GenerateDecoderOnlyOutput(sequences=seq)
        except Exception:  # pragma: no cover - very old/new transformers without that class
            return seq