File size: 20,973 Bytes
dbc600c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
# ============================================================================
# modeling_captionbert.py -- AbstractPhil/captionbert-8192-v2
#
#   from transformers import AutoModel, AutoTokenizer
#   model = AutoModel.from_pretrained("AbstractPhil/captionbert-8192-v2",
#                                     trust_remote_code=True)
#   tok   = AutoTokenizer.from_pretrained("google-bert/bert-base-uncased")
#   out   = model(**tok(["a cat on a windowsill"], return_tensors="pt"))
#   emb   = out.pooler_output          # (B, 768) L2-normalized
#
# OR just:  emb = model.encode(["a cat on a windowsill"])
#
# WITH AMOE ARMS (needs amoe-lora; imported lazily so the base model loads
# without it):
#   model.attach_amoe()                # shipped 3-arm collective
#   emb = model.encode([...])          # adapted
#   with model.amoe_off():             # the unsupervised baseline
#       base = model.encode([...])
#   model.set_amoe(["equiv"])          # one arm, DAMPED inside the dispatch
#   model.detach_amoe()                # bit-exact restore, asserted
#
# ---------------------------------------------------------------------------
# BREAKING CHANGE FROM v1 -- READ THIS IF YOU USED geolip-captionbert-8192
#   v1 returned the POOLED 768-d embedding as `last_hidden_state`. That is not
#   the transformers convention and it silently breaks anything expecting token
#   states. v2 follows the convention:
#       last_hidden_state : (B, L, 512)  token states
#       pooler_output     : (B, 768)     L2-normalized embedding  <-- the product
#       embedding         : (B, 768)     alias for pooler_output
#   If you are porting v1 code, `last_hidden_state` -> `pooler_output`.
#
#   v1 also shipped an AlignmentBank. v2 does NOT. Measured on v1: the bank's
#   expert-consistency block varied 0.2% across samples and took 0.23% of its
#   projection energy while anchor distances took 98.70% -- because
#   `back = x @ R.T @ R` is a rotation round-trip and carries no data. Content
#   extensions belong in an AMOE anchor, which this repo ships separately.
# ============================================================================

import os
from dataclasses import dataclass
from types import SimpleNamespace
from typing import List, Optional, Tuple, Union

import torch
import torch.nn as nn
import torch.nn.functional as F
from transformers import PretrainedConfig, PreTrainedModel
from transformers.modeling_outputs import BaseModelOutputWithPooling


class CaptionBertV2Config(PretrainedConfig):
    model_type = "captionbert_v2"

    def __init__(
        self,
        vocab_size: int = 30522,
        hidden_size: int = 512,          # d_model
        num_hidden_layers: int = 12,
        num_attention_heads: int = 8,
        intermediate_size: int = 2048,
        output_dim: int = 768,           # consensus space
        max_position_embeddings: int = 8192,
        hidden_dropout_prob: float = 0.1,
        pad_token_id: int = 0,
        pooling: str = "mean",           # "mean" | "cls"
        **kwargs,
    ):
        super().__init__(pad_token_id=pad_token_id, **kwargs)
        self.vocab_size = vocab_size
        self.hidden_size = hidden_size
        self.num_hidden_layers = num_hidden_layers
        self.num_attention_heads = num_attention_heads
        self.intermediate_size = intermediate_size
        self.output_dim = output_dim
        self.max_position_embeddings = max_position_embeddings
        self.hidden_dropout_prob = hidden_dropout_prob
        self.pooling = pooling


class CaptionBertV2Model(PreTrainedModel):
    """
    Standalone caption/sentence encoder distilled from the geometric consensus
    of five BERT-family teachers. No expert models at inference.

    Parameter names are deliberately NOT namespaced under a submodule so that
    the training checkpoint loads unchanged: token_emb, pos_emb, emb_norm,
    encoder.layers.*, output_proj.*.
    """

    config_class = CaptionBertV2Config
    base_model_prefix = "captionbert_v2"
    supports_gradient_checkpointing = True

    def __init__(self, config: CaptionBertV2Config):
        super().__init__(config)
        d = config.hidden_size
        self.token_emb = nn.Embedding(config.vocab_size, d,
                                      padding_idx=config.pad_token_id)
        self.pos_emb = nn.Embedding(config.max_position_embeddings, d)
        self.emb_norm = nn.LayerNorm(d)
        self.emb_drop = nn.Dropout(config.hidden_dropout_prob)
        layer = nn.TransformerEncoderLayer(
            d_model=d,
            nhead=config.num_attention_heads,
            dim_feedforward=config.intermediate_size,
            dropout=config.hidden_dropout_prob,
            activation="gelu",
            batch_first=True,
            norm_first=True,
        )
        self.encoder = nn.TransformerEncoder(
            layer, num_layers=config.num_hidden_layers, enable_nested_tensor=False)
        self.output_proj = nn.Sequential(
            nn.Linear(d, d), nn.GELU(), nn.LayerNorm(d), nn.Linear(d, config.output_dim))
        self.post_init()

    # -- HF plumbing --
    def get_input_embeddings(self):
        return self.token_emb

    def set_input_embeddings(self, value):
        self.token_emb = value

    def forward(
        self,
        input_ids: torch.LongTensor = None,
        attention_mask: Optional[torch.Tensor] = None,
        output_hidden_states: Optional[bool] = None,
        return_dict: Optional[bool] = None,
        **kwargs,
    ) -> Union[Tuple, BaseModelOutputWithPooling]:
        return_dict = return_dict if return_dict is not None else True
        L = input_ids.shape[1]
        pos = torch.arange(L, device=input_ids.device).unsqueeze(0)
        x = self.emb_drop(self.emb_norm(self.token_emb(input_ids) + self.pos_emb(pos)))

        kpm = (~attention_mask.bool()) if attention_mask is not None \
            else (input_ids == self.config.pad_token_id)

        hidden = [x] if output_hidden_states else None
        # Iterate the layers directly rather than calling self.encoder(...):
        # nn.TransformerEncoder's fast path inspects layer types, and an AMOE
        # anchor wraps each layer in a BlockWithAdapter that is not a
        # TransformerEncoderLayer. This keeps attach() a drop-in.
        for mod in self.encoder.layers:
            x = mod(x, src_key_padding_mask=kpm)
            if output_hidden_states:
                hidden.append(x)
        if self.encoder.norm is not None:
            x = self.encoder.norm(x)

        if self.config.pooling == "cls":
            pooled = x[:, 0]
        else:
            m = (attention_mask.unsqueeze(-1).to(x.dtype) if attention_mask is not None
                 else (~kpm).unsqueeze(-1).to(x.dtype))
            pooled = (x * m).sum(1) / m.sum(1).clamp(min=1)
        embedding = F.normalize(self.output_proj(pooled), dim=-1)

        if not return_dict:
            return (x, embedding) + ((tuple(hidden),) if output_hidden_states else ())
        out = BaseModelOutputWithPooling(
            last_hidden_state=x,                       # (B, L, 512) token states
            pooler_output=embedding,                   # (B, 768) THE PRODUCT
            hidden_states=tuple(hidden) if output_hidden_states else None,
        )
        out.embedding = embedding                      # explicit alias
        return out

    @torch.no_grad()
    def encode(self, texts, tokenizer=None, batch_size: int = 128,
               max_length: int = 256, device=None) -> torch.Tensor:
        """Raw text -> (N, 768) L2-normalized embeddings."""
        if isinstance(texts, str):
            texts = [texts]
        if tokenizer is None:
            from transformers import AutoTokenizer
            tokenizer = AutoTokenizer.from_pretrained("google-bert/bert-base-uncased")
        device = device or next(self.parameters()).device
        was_training = self.training
        self.eval()
        out = []
        for i in range(0, len(texts), batch_size):
            t = tokenizer(list(texts[i:i + batch_size]), max_length=max_length,
                          padding=True, truncation=True, return_tensors="pt").to(device)
            out.append(self(**t).pooler_output.float().cpu())
        if was_training:
            self.train()
        return torch.cat(out)


# ---------------------------------------------------------------------------
# AMOE binding -- lets amoe-lora attach anchors to this trunk unmodified.
#
#   import amoe
#   from modeling_captionbert import CaptionBertV2Binding
#   h = amoe.attach(model, "amoe/moe/equiv.anchor.pt",
#                   binding=CaptionBertV2Binding(d=model.config.hidden_size))
#
# amoe's PathBinding would find encoder.layers by dotted path but then read
# model.config.hidden_size -- which works here because this IS a
# PretrainedConfig. The explicit binding is kept for plain-nn.Module use.
# ---------------------------------------------------------------------------

@dataclass
class CaptionBertV2Binding:
    d: int = 512
    name: str = "captionbert_v2"

    def layers(self, model):
        return model.encoder.layers

    def set_layers(self, model, new):
        model.encoder.layers = nn.ModuleList(new)

    def hidden_size(self, model) -> int:
        return int(self.d)


# ---------------------------------------------------------------------------
# AMOE ARMS -- attach / toggle / detach, from the same AutoModel object.
#
#   model = AutoModel.from_pretrained(REPO, trust_remote_code=True)
#   model.attach_amoe()                       # shipped 3-arm collective
#   emb = model.encode(["a cat on a windowsill"])
#   with model.amoe_off():                    # the unsupervised baseline
#       base = model.encode(["a cat on a windowsill"])
#   model.set_amoe(["equiv"])                 # one arm inside the dispatch
#   model.detach_amoe()                       # bit-exact restore, asserted
#
# `amoe` is imported LAZILY: this file is executed by every
# AutoModel.from_pretrained(trust_remote_code=True), and the base model must
# load on a machine that has never heard of amoe-lora.
#
# Masking never renormalizes (the damping law), so a single arm inside the
# dispatch is DAMPED and reads lower than that arm trained alone. That is the
# mechanism, not a bug -- see the model card.
# ---------------------------------------------------------------------------

# AMOE_REPO defaults to THE REPO THIS MODEL WAS LOADED FROM, not a hardcoded
# name. Anchors are TRUNK-BOUND: measured 2026-08-02, the captionbert-8192-v2
# arms lose 31% of their gain on the -B trunk (.7287 -> .6863), and re-aligning
# the routing keys alone recovers only 29% of that. Retrained natively they
# reach .7294. So a hardcoded arms repo silently hands a trunk the wrong arms.
AMOE_REPO = None            # None -> config._name_or_path
AMOE_LIB = "amoe/arms"
# dispatch checkpoints are searched in order; the first that exists wins
AMOE_DISPATCH_CANDIDATES = (
    "amoe/b-collective/captionbert-b-arms-native.dispatch.pt",
    "amoe/collective/captionbert-v2-collective.dispatch.pt",
)
AMOE_DEFAULT_DISPATCH = None   # None -> first candidate present in the repo
AMOE_FALLBACKS = lambda n: [f"amoe/b-collective/{n}.anchor.pt",
                            f"amoe/collective/{n}.anchor.pt",
                            f"amoe/moe/{n}.anchor.pt"]
_AMOE_HINT = ("amoe-lora is required for arms: "
              "pip install git+https://github.com/AbstractEyes/amoe-lora")


def _amoe():
    try:
        from amoe.core.adapter import AdapterSpec, RelayPatchwork
        from amoe.core.dispatch import AnchorDispatch, BlockWithDispatch
        from amoe.io.checkpoint import load_anchor, load_dispatch
    except ImportError as e:
        raise ImportError(_AMOE_HINT) from e
    return SimpleNamespace(AdapterSpec=AdapterSpec, RelayPatchwork=RelayPatchwork,
                           AnchorDispatch=AnchorDispatch,
                           BlockWithDispatch=BlockWithDispatch,
                           load_anchor=load_anchor, load_dispatch=load_dispatch)


class _ArmsOff:
    def __init__(self, model):
        self.m = model

    def __enter__(self):
        self._prev = [list(d.enabled) for d in self.m._amoe_dispatches]
        for d in self.m._amoe_dispatches:
            d.enabled = [False] * len(d.enabled)
        return self.m

    def __exit__(self, *exc):
        for d, p in zip(self.m._amoe_dispatches, self._prev):
            d.enabled = list(p)


def _attach_amoe(self, arms=None, dispatch=None, repo=None, tau=None, verify=True):
    """
    Attach AMOE arms under a trained dispatch.

    arms      list of arm names. None -> the dispatch checkpoint's own roster,
              in ITS order (the routing keys are per-arm and positional, so the
              order is not cosmetic).
    dispatch  path in `repo`, a local file, or None for an untrained dispatch.
    verify    assert that all-arms-disabled reproduces the bare trunk.
    """
    from huggingface_hub import hf_hub_download, HfApi
    A = _amoe()
    if getattr(self, "_amoe_original_layers", None) is not None:
        raise RuntimeError("arms already attached; call detach_amoe() first")

    # resolve the repo: this model's own, unless told otherwise
    if repo is None:
        repo = AMOE_REPO or getattr(self.config, "_name_or_path", None)
    if not repo:
        raise ValueError("cannot determine the arms repo; pass repo=...")
    # resolve the dispatch: first candidate actually present in THAT repo
    if dispatch is None:
        dispatch = AMOE_DEFAULT_DISPATCH
    if dispatch is None:
        try:
            present = set(HfApi().list_repo_files(repo))
        except Exception:
            present = set()
        dispatch = next((c for c in AMOE_DISPATCH_CANDIDATES if c in present), None)
        if dispatch is None:
            local = os.path.isdir(str(repo))
            hint = (f"'{repo}' is a LOCAL DIRECTORY, so there is no hub repo to "
                    f"search. Pass the repo explicitly:\n"
                    f"    model.attach_amoe(repo='AbstractPhil/captionbert-8192-v2-B')"
                    if local else
                    f"Pass dispatch=<path in {repo}>, or dispatch=False to attach "
                    f"an UNTRAINED dispatch.")
            raise FileNotFoundError(
                f"no dispatch found in {repo}. Tried "
                f"{list(AMOE_DISPATCH_CANDIDATES)}.\n         {hint}")
        print(f"[amoe] dispatch: {repo}/{dispatch}")
    if dispatch is False:
        dispatch = None

    dck = None
    if dispatch is not None:
        p = dispatch if os.path.exists(dispatch) else hf_hub_download(repo, dispatch)
        dck = A.load_dispatch(p)
        if arms is None:
            arms = list(dck.meta.get("anchors", []))
        if tau is None:
            tau = float(dck.meta.get("tau", 0.1))
    if not arms:
        raise ValueError("no arms given and the dispatch names none")
    tau = 0.1 if tau is None else tau

    cks, resolved = [], []
    for name in arms:
        if os.path.exists(str(name)):
            path = str(name)
        else:
            # amoe/arms/ is the canonical library, but it is published by a
            # consolidation step that may not have run yet. Fall back to the
            # campaign folders the anchors were originally trained into, and
            # say which one answered.
            path = None
            for cand in ([f"{AMOE_LIB}/{name}.anchor.pt"] + AMOE_FALLBACKS(name)):
                try:
                    path = hf_hub_download(repo, cand)
                    resolved.append(cand)
                    break
                except Exception:
                    continue
            if path is None:
                raise FileNotFoundError(
                    f"arm '{name}' not found in {repo}: tried {AMOE_LIB}/ and "
                    f"{AMOE_FALLBACKS(name)}. Pass an explicit path, or publish "
                    f"the arm library.")
        cks.append(A.load_anchor(path))
    if resolved and not all(r.startswith(AMOE_LIB) for r in resolved):
        print(f"[amoe] resolved outside {AMOE_LIB}: "
              f"{[r for r in resolved if not r.startswith(AMOE_LIB)]}")

    d = self.config.hidden_size
    layers = list(self.encoder.layers)
    self._amoe_original_layers = layers
    dev = next(self.parameters()).device
    new, disps = [], []
    for i, layer in enumerate(layers):
        stack = nn.ModuleList()
        for ck in cks:
            a = A.RelayPatchwork(d, A.AdapterSpec())
            a.load_state_dict({k[len(f"{i}."):]: v for k, v in ck.adapters.items()
                               if k.startswith(f"{i}.")})
            for q in a.parameters():
                q.requires_grad_(False)
            stack.append(a)
        dp = A.AnchorDispatch(stack.to(dev), d,
                              emb=int(dck.meta.get("emb", 64)) if dck else 64,
                              tau=tau).to(dev)
        if dck is not None:
            with torch.no_grad():
                dp.dispatch.copy_(dck.dispatch[i]["dispatch"].to(dev))
                dp.key_proj.copy_(dck.dispatch[i]["key_proj"].to(dev))
        for q in dp.parameters():
            q.requires_grad_(False)
        disps.append(dp)
        new.append(A.BlockWithDispatch(layer, dp))
    self.encoder.layers = nn.ModuleList(new)
    self._amoe_dispatches = disps
    self._amoe_names = list(arms)

    if verify:
        ids = (torch.arange(8, device=dev).unsqueeze(0) % 7 + 1)
        am = torch.ones_like(ids)
        was = self.training
        self.eval()
        wrapped_layers = self.encoder.layers
        try:
            with torch.no_grad():
                with self.amoe_off():
                    off = self(input_ids=ids,
                               attention_mask=am).pooler_output.clone()
                # temporarily restore the bare stack for the reference forward;
                # try/finally so a raise here cannot strand the model holding
                # unwrapped layers while _amoe_dispatches still points at arms.
                self.encoder.layers = nn.ModuleList(layers)
                pure = self(input_ids=ids,
                            attention_mask=am).pooler_output.clone()
        finally:
            self.encoder.layers = wrapped_layers
            if was:
                self.train()
        gap = (off - pure).abs().max().item()
        if gap > 1e-6:
            raise RuntimeError(
                f"TOGGLE LAW VIOLATED: arms disabled shift the trunk by {gap:.3e}. "
                "Do not trust any on/off comparison from this attachment.")
    return self


def _set_amoe(self, arms):
    """Enable a subset by name (or a boolean mask in attach order)."""
    if getattr(self, "_amoe_dispatches", None) is None:
        raise RuntimeError("no arms attached")
    if arms is None:
        mask = [True] * len(self._amoe_names)
    elif all(isinstance(x, bool) for x in arms):
        mask = list(arms)
    else:
        unknown = [a for a in arms if a not in self._amoe_names]
        if unknown:
            raise ValueError(f"unknown arms {unknown}; have {self._amoe_names}")
        mask = [n in arms for n in self._amoe_names]
    for d in self._amoe_dispatches:
        d.enabled = list(mask)
    return self


def _amoe_off(self):
    if getattr(self, "_amoe_dispatches", None) is None:
        raise RuntimeError("no arms attached")
    return _ArmsOff(self)


def _detach_amoe(self, verify=True):
    """Restore the bare trunk. Bit-exact by construction; asserted by default."""
    orig = getattr(self, "_amoe_original_layers", None)
    if orig is None:
        return self
    dev = next(self.parameters()).device
    if verify:
        was = self.training
        self.eval()
        ids = (torch.arange(8, device=dev).unsqueeze(0) % 7 + 1)
        am = torch.ones_like(ids)
        with torch.no_grad():
            with self.amoe_off():
                off = self(input_ids=ids, attention_mask=am).pooler_output.clone()
        self.encoder.layers = nn.ModuleList(orig)
        with torch.no_grad():
            pure = self(input_ids=ids, attention_mask=am).pooler_output.clone()
        if was:
            self.train()
        gap = (off - pure).abs().max().item()
        if gap > 1e-6:
            raise RuntimeError(f"detach is not bit-exact: {gap:.3e}")
    else:
        self.encoder.layers = nn.ModuleList(orig)
    self._amoe_original_layers = None
    self._amoe_dispatches = None
    self._amoe_names = None
    return self


def _amoe_arms(self):
    """Names of the attached arms, in dispatch order. Empty if none."""
    return list(getattr(self, "_amoe_names", None) or [])


CaptionBertV2Model.attach_amoe = _attach_amoe
CaptionBertV2Model.set_amoe = _set_amoe
CaptionBertV2Model.amoe_off = _amoe_off
CaptionBertV2Model.detach_amoe = _detach_amoe
CaptionBertV2Model.amoe_arms = property(_amoe_arms)

CaptionBertV2Config.register_for_auto_class()
CaptionBertV2Model.register_for_auto_class("AutoModel")