abtonmoy commited on
Commit
26319a8
·
verified ·
1 Parent(s): f482f57

AutoModel + trust_remote_code support (remote-code loading; existing loaders unchanged)

Browse files
config.json CHANGED
@@ -50,5 +50,27 @@
50
  "a2t_R@10": 0.8849999904632568,
51
  "a2t_mAP@10": 0.587212324142456
52
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  }
54
  }
 
50
  "a2t_R@10": 0.8849999904632568,
51
  "a2t_mAP@10": 0.587212324142456
52
  }
53
+ },
54
+ "d_audio": 3584,
55
+ "resampler_depth": 6,
56
+ "resampler_heads": 8,
57
+ "resampler_ffn_mult": 4,
58
+ "resampler_dropout": 0.0,
59
+ "adapter_act": "silu",
60
+ "mrl_default": 1024,
61
+ "audio_pad_id": 151654,
62
+ "eos_id": 151645,
63
+ "pad_id": 151643,
64
+ "audio_pad_token": "<|audio_pad|>",
65
+ "base_model": "Qwen/Qwen3-VL-Embedding-2B",
66
+ "audio_model": "Qwen/Qwen2.5-Omni-7B",
67
+ "max_text_tokens": 512,
68
+ "n_decoder_layers": 28,
69
+ "architectures": [
70
+ "FusionEmbeddingModel"
71
+ ],
72
+ "auto_map": {
73
+ "AutoConfig": "configuration_fusion_embedding.FusionEmbeddingConfig",
74
+ "AutoModel": "modeling_fusion_embedding.FusionEmbeddingModel"
75
  }
76
  }
configuration_fusion_embedding.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """HF remote-code configuration for the fusion-embedding family.
2
+
3
+ Lives on the model repos (EximiusLabs/fusion-embedding-1-2b-preview and
4
+ EximiusLabs/fusion-embedding-2-2b-preview) next to modeling_fusion_embedding.py so the
5
+ models load with plain transformers:
6
+
7
+ from transformers import AutoModel
8
+ model = AutoModel.from_pretrained(
9
+ "EximiusLabs/fusion-embedding-1-2b-preview", trust_remote_code=True)
10
+
11
+ The config carries the trained-connector dimensions plus the frozen-component repo
12
+ names; the frozen Qwen3-VL-Embedding base and Qwen2.5-Omni audio tower are NOT part of
13
+ this checkpoint — they are fetched from their own repositories at first use.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from transformers import PretrainedConfig
19
+
20
+
21
+ class FusionEmbeddingConfig(PretrainedConfig):
22
+ model_type = "fusion-embedding-connector"
23
+
24
+ def __init__(
25
+ self,
26
+ d_audio: int = 3584,
27
+ d_llm: int = 2048,
28
+ n_query: int = 64,
29
+ d_resampler: int = 384,
30
+ resampler_depth: int = 6,
31
+ resampler_heads: int = 8,
32
+ resampler_ffn_mult: int = 4,
33
+ resampler_dropout: float = 0.0,
34
+ adapter_rank: int = 0,
35
+ adapter_act: str = "silu",
36
+ mrl_dims=(2048, 1536, 1024, 512, 256, 128, 64),
37
+ mrl_default: int = 1024,
38
+ audio_pad_id: int = 151654,
39
+ eos_id: int = 151645,
40
+ pad_id: int = 151643,
41
+ audio_pad_token: str = "<|audio_pad|>",
42
+ base_model: str = "Qwen/Qwen3-VL-Embedding-2B",
43
+ audio_model: str = "Qwen/Qwen2.5-Omni-7B",
44
+ max_text_tokens: int = 512,
45
+ n_decoder_layers: int = 28,
46
+ **kwargs,
47
+ ):
48
+ self.d_audio = d_audio
49
+ self.d_llm = d_llm
50
+ self.n_query = n_query
51
+ self.d_resampler = d_resampler
52
+ self.resampler_depth = resampler_depth
53
+ self.resampler_heads = resampler_heads
54
+ self.resampler_ffn_mult = resampler_ffn_mult
55
+ self.resampler_dropout = resampler_dropout
56
+ self.adapter_rank = adapter_rank or 0
57
+ self.adapter_act = adapter_act or "silu"
58
+ self.mrl_dims = list(mrl_dims)
59
+ self.mrl_default = mrl_default
60
+ self.audio_pad_id = audio_pad_id
61
+ self.eos_id = eos_id
62
+ self.pad_id = pad_id
63
+ self.audio_pad_token = audio_pad_token
64
+ self.base_model = base_model
65
+ self.audio_model = audio_model
66
+ self.max_text_tokens = max_text_tokens
67
+ self.n_decoder_layers = n_decoder_layers
68
+ super().__init__(**kwargs)
modeling_fusion_embedding.py ADDED
@@ -0,0 +1,521 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """HF remote-code modeling for the fusion-embedding family (AutoModel + trust_remote_code).
2
+
3
+ One embedding space for text, images, and audio. The checkpoint on this repository holds
4
+ ONLY the trained components (perceiver-resampler connector, diagonal text whitening,
5
+ logit scale and — generation 2 — the modality-gated deep adapters); the frozen
6
+ Qwen3-VL-Embedding-2B base and the frozen Qwen2.5-Omni audio tower are downloaded from
7
+ their own repositories on first use and are byte-identical to their releases.
8
+
9
+ from transformers import AutoModel
10
+ model = AutoModel.from_pretrained(
11
+ "EximiusLabs/fusion-embedding-1-2b-preview", trust_remote_code=True)
12
+ t = model.embed_text("a dog barks in the distance")
13
+ a = model.embed_audio("dog.wav")
14
+ i = model.embed_image("dog.jpg")
15
+
16
+ The embed_* methods reproduce the repository's reference ``inference.py`` exactly (same
17
+ chat templates, truncation, pooling, whitening, Matryoshka truncation and normalization);
18
+ outputs are bitwise-identical to that loader on the same hardware. Non-audio inputs never
19
+ execute the generation-2 adapter branch (the gate returns the frozen layers' output
20
+ untouched), so text/image outputs are bit-for-bit those of generation 1 and of the base's
21
+ computation path.
22
+
23
+ Requires: transformers>=4.46 (with the Qwen2.5-Omni model classes), torchvision, pillow,
24
+ soundfile, librosa. A CUDA GPU is recommended (~14 GB at bf16).
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import math
30
+ import os
31
+ from typing import Optional, Union
32
+
33
+ import torch
34
+ import torch.nn as nn
35
+ import torch.nn.functional as F
36
+ from transformers import PreTrainedModel
37
+
38
+ from .configuration_fusion_embedding import FusionEmbeddingConfig
39
+
40
+ DEFAULT_QUERY_INSTRUCTION = "Retrieve images or text relevant to the user's query."
41
+ DOC_INSTRUCTION = "Represent the user's input."
42
+
43
+ _ACTS = {"silu": nn.SiLU, "gelu": nn.GELU, "relu": nn.ReLU}
44
+
45
+
46
+ # --------------------------------------------------------------------------- #
47
+ # helpers (mirrors of the training package, kept self-contained on purpose)
48
+ # --------------------------------------------------------------------------- #
49
+ def _chat(instruction: str, user_content: str) -> str:
50
+ """The base's official embedding format: system-turn instruction, assistant opener."""
51
+ return (f"<|im_start|>system\n{instruction}<|im_end|>\n"
52
+ f"<|im_start|>user\n{user_content}<|im_end|>\n"
53
+ f"<|im_start|>assistant\n")
54
+
55
+
56
+ def sinusoidal_positions(length: int, dim: int, device, dtype) -> torch.Tensor:
57
+ if dim % 2 != 0:
58
+ pe = sinusoidal_positions(length, dim + 1, device, dtype)
59
+ return pe[:, :dim]
60
+ pos = torch.arange(length, device=device, dtype=torch.float32).unsqueeze(1)
61
+ div = torch.exp(torch.arange(0, dim, 2, device=device, dtype=torch.float32)
62
+ * (-math.log(10000.0) / dim))
63
+ pe = torch.zeros(length, dim, device=device, dtype=torch.float32)
64
+ pe[:, 0::2] = torch.sin(pos * div)
65
+ pe[:, 1::2] = torch.cos(pos * div)
66
+ return pe.to(dtype)
67
+
68
+
69
+ def last_token_pool(hidden: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
70
+ lengths = attention_mask.long().sum(dim=1) - 1
71
+ lengths = lengths.clamp(min=0)
72
+ idx = lengths.view(-1, 1, 1).expand(-1, 1, hidden.size(-1))
73
+ return hidden.gather(1, idx).squeeze(1)
74
+
75
+
76
+ def mrl_truncate_normalize(x: torch.Tensor, dim: int) -> torch.Tensor:
77
+ return F.normalize(x[..., :dim], p=2, dim=-1)
78
+
79
+
80
+ class TextWhitening(nn.Module):
81
+ """Diagonal (per-dim, MRL-safe) standardization of frozen text embeddings."""
82
+
83
+ def __init__(self, dim: int):
84
+ super().__init__()
85
+ self.register_buffer("mean", torch.zeros(dim))
86
+ self.register_buffer("std", torch.ones(dim))
87
+ self.register_buffer("fitted", torch.zeros((), dtype=torch.uint8))
88
+
89
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
90
+ if int(self.fitted) == 0:
91
+ return x
92
+ mean = self.mean.to(device=x.device, dtype=x.dtype)
93
+ std = self.std.to(device=x.device, dtype=x.dtype)
94
+ return (x - mean) / std
95
+
96
+
97
+ class _ResamplerBlock(nn.Module):
98
+ """Pre-norm: latent self-attention -> cross-attention -> FFN."""
99
+
100
+ def __init__(self, dim: int, heads: int, ffn_mult: int, dropout: float):
101
+ super().__init__()
102
+ self.norm_sa = nn.LayerNorm(dim)
103
+ self.self_attn = nn.MultiheadAttention(dim, heads, dropout=dropout, batch_first=True)
104
+ self.norm_q = nn.LayerNorm(dim)
105
+ self.norm_kv = nn.LayerNorm(dim)
106
+ self.cross_attn = nn.MultiheadAttention(dim, heads, dropout=dropout, batch_first=True)
107
+ self.norm_ff = nn.LayerNorm(dim)
108
+ self.ffn = nn.Sequential(
109
+ nn.Linear(dim, dim * ffn_mult),
110
+ nn.GELU(),
111
+ nn.Dropout(dropout),
112
+ nn.Linear(dim * ffn_mult, dim),
113
+ )
114
+
115
+ def forward(self, q, kv, key_padding_mask):
116
+ h = self.norm_sa(q)
117
+ q = q + self.self_attn(h, h, h, need_weights=False)[0]
118
+ h = self.norm_q(q)
119
+ kv_n = self.norm_kv(kv)
120
+ q = q + self.cross_attn(h, kv_n, kv_n, key_padding_mask=key_padding_mask,
121
+ need_weights=False)[0]
122
+ q = q + self.ffn(self.norm_ff(q))
123
+ return q
124
+
125
+
126
+ class FusionResampler(nn.Module):
127
+ """Perceiver-resampler: variable-length audio frames -> N fixed latent tokens."""
128
+
129
+ def __init__(self, cfg: FusionEmbeddingConfig):
130
+ super().__init__()
131
+ dr = cfg.d_resampler
132
+ self.in_proj = nn.Linear(cfg.d_audio, dr)
133
+ self.queries = nn.Parameter(torch.empty(cfg.n_query, dr))
134
+ nn.init.normal_(self.queries, std=0.02)
135
+ self.blocks = nn.ModuleList(
136
+ _ResamplerBlock(dr, cfg.resampler_heads, cfg.resampler_ffn_mult,
137
+ cfg.resampler_dropout)
138
+ for _ in range(cfg.resampler_depth)
139
+ )
140
+ self.out_proj = nn.Linear(dr, cfg.d_llm)
141
+ self.out_norm = nn.LayerNorm(cfg.d_llm)
142
+
143
+ def forward(self, frames: torch.Tensor, frame_mask: Optional[torch.Tensor] = None):
144
+ B, T, _ = frames.shape
145
+ if frame_mask is None:
146
+ frame_mask = torch.ones(B, T, dtype=torch.bool, device=frames.device)
147
+ kv = self.in_proj(frames)
148
+ kv = kv + sinusoidal_positions(T, kv.size(-1), kv.device, kv.dtype).unsqueeze(0)
149
+ key_padding = ~frame_mask
150
+ fully_masked = key_padding.all(dim=1)
151
+ if fully_masked.any():
152
+ key_padding = key_padding.clone()
153
+ key_padding[fully_masked, 0] = False
154
+ q = self.queries.unsqueeze(0).expand(B, -1, -1)
155
+ for block in self.blocks:
156
+ q = block(q, kv, key_padding)
157
+ return self.out_norm(self.out_proj(q))
158
+
159
+
160
+ class AdapterGate:
161
+ """Depth-counted on/off switch shared by every adapter hook (generation 2)."""
162
+
163
+ __slots__ = ("_depth",)
164
+
165
+ def __init__(self) -> None:
166
+ self._depth = 0
167
+
168
+ @property
169
+ def active(self) -> bool:
170
+ return self._depth > 0
171
+
172
+ def __enter__(self) -> "AdapterGate":
173
+ self._depth += 1
174
+ return self
175
+
176
+ def __exit__(self, *exc) -> None:
177
+ self._depth -= 1
178
+ if self._depth < 0:
179
+ raise RuntimeError("AdapterGate depth underflow — unbalanced enter/exit")
180
+
181
+
182
+ class GatedAdapter(nn.Module):
183
+ """Parallel bottleneck adapter: ``h + up(act(down(LN(h))))``, computed in fp32."""
184
+
185
+ def __init__(self, d_model: int, rank: int, act: str = "silu"):
186
+ super().__init__()
187
+ self.norm = nn.LayerNorm(d_model)
188
+ self.down = nn.Linear(d_model, rank, bias=False)
189
+ self.act = _ACTS[act]()
190
+ self.up = nn.Linear(rank, d_model, bias=False)
191
+ nn.init.zeros_(self.up.weight)
192
+
193
+ def forward(self, h: torch.Tensor) -> torch.Tensor:
194
+ return self.up(self.act(self.down(self.norm(h.float())))).to(h.dtype)
195
+
196
+
197
+ def _make_hook(adapter: GatedAdapter, gate: AdapterGate):
198
+ def hook(_module, _inputs, output):
199
+ if not gate.active:
200
+ return None # keep original output — bitwise no-op
201
+ if isinstance(output, tuple): # HF decoder layers -> (hidden, ...)
202
+ h = output[0]
203
+ return (h + adapter(h),) + tuple(output[1:])
204
+ return output + adapter(output)
205
+ return hook
206
+
207
+
208
+ class OmniAudioAdapter(nn.Module):
209
+ """Frozen Qwen2.5-Omni audio encoder -> (frames [B,T,d_audio], frame_mask [B,T])."""
210
+
211
+ def __init__(self, encoder: nn.Module, d_audio: int):
212
+ super().__init__()
213
+ self.encoder = encoder
214
+ self.d_audio = d_audio
215
+
216
+ @torch.no_grad()
217
+ def forward(self, mel: torch.Tensor, mel_mask: Optional[torch.Tensor] = None):
218
+ B, n_mels, Fdim = mel.shape
219
+ if mel_mask is None:
220
+ feat_lens = torch.full((B,), Fdim, dtype=torch.long, device=mel.device)
221
+ else:
222
+ feat_lens = mel_mask.long().sum(dim=1)
223
+ dtype = next(self.encoder.parameters()).dtype
224
+ per_item = []
225
+ for i in range(B):
226
+ Li = max(int(feat_lens[i].item()), 1)
227
+ feats = mel[i, :, :Li].to(dtype)
228
+ out = self.encoder(input_features=feats,
229
+ feature_lens=torch.tensor([Li], device=mel.device))
230
+ frames = out.last_hidden_state
231
+ if frames.dim() == 3:
232
+ frames = frames[0]
233
+ per_item.append(frames.float())
234
+ T_max = max(f.shape[0] for f in per_item)
235
+ frames_out = mel.new_zeros(B, T_max, self.d_audio)
236
+ frame_mask = torch.zeros(B, T_max, dtype=torch.bool, device=mel.device)
237
+ for i, f in enumerate(per_item):
238
+ frames_out[i, : f.shape[0]] = f
239
+ frame_mask[i, : f.shape[0]] = True
240
+ return frames_out, frame_mask
241
+
242
+
243
+ # --------------------------------------------------------------------------- #
244
+ # the AutoModel entry point
245
+ # --------------------------------------------------------------------------- #
246
+ class FusionEmbeddingModel(PreTrainedModel):
247
+ """fusion-embedding for transformers AutoModel (trust_remote_code).
248
+
249
+ The registered submodules are exactly the trained components shipped in this
250
+ repository's ``model.safetensors`` (resampler + text whitening + logit scale
251
+ + generation-2 adapters). The frozen base and audio tower load lazily from
252
+ their own repositories on the first ``embed_*`` call, onto the device the
253
+ trained components are on at that moment — call ``.to("cuda")`` (or pass
254
+ ``device_map``) before embedding.
255
+ """
256
+
257
+ config_class = FusionEmbeddingConfig
258
+ base_model_prefix = "fusion_embedding"
259
+ main_input_name = "input_ids"
260
+ _supports_flash_attn_2 = False
261
+
262
+ def __init__(self, config: FusionEmbeddingConfig):
263
+ super().__init__(config)
264
+ self.resampler = FusionResampler(config)
265
+ self.text_whitening = TextWhitening(config.d_llm)
266
+ self.logit_scale = nn.Parameter(torch.zeros(1))
267
+ self.audio_adapters: Optional[nn.ModuleList] = None
268
+ if config.adapter_rank and config.adapter_rank > 0:
269
+ self.audio_adapters = nn.ModuleList(
270
+ GatedAdapter(config.d_llm, config.adapter_rank, config.adapter_act)
271
+ for _ in range(config.n_decoder_layers)
272
+ )
273
+ # runtime-only state (plain dict: never in state_dict / parameters / save)
274
+ self._rt: dict = {}
275
+ self.post_init()
276
+
277
+ def _init_weights(self, module): # trained weights always come from the checkpoint
278
+ pass
279
+
280
+ # ------------------------------------------------------------- backbones
281
+ @property
282
+ def _device(self) -> torch.device:
283
+ return self.resampler.out_proj.weight.device
284
+
285
+ def _ensure_backbones(self) -> None:
286
+ if "full" in self._rt:
287
+ return
288
+ from transformers import (AutoConfig, AutoFeatureExtractor, AutoModel,
289
+ AutoProcessor)
290
+
291
+ device, dtype = self._device, torch.bfloat16
292
+ full = AutoModel.from_pretrained(self.config.base_model, trust_remote_code=True,
293
+ dtype=dtype)
294
+ full = full.to(device).eval()
295
+ for p in full.parameters():
296
+ p.requires_grad_(False)
297
+ proc = AutoProcessor.from_pretrained(self.config.base_model, trust_remote_code=True)
298
+
299
+ acfg = AutoConfig.from_pretrained(self.config.audio_model, trust_remote_code=True)
300
+ audio_cfg = acfg.thinker_config.audio_config
301
+ tower = self._load_audio_encoder(audio_cfg, dtype).to(device)
302
+ fe_audio = AutoFeatureExtractor.from_pretrained(self.config.audio_model,
303
+ trust_remote_code=True)
304
+
305
+ self._rt.update(full=full, proc=proc, tok=proc.tokenizer,
306
+ tower=OmniAudioAdapter(tower, self.config.d_audio),
307
+ fe_audio=fe_audio, gate=AdapterGate(), adapter_handles=[])
308
+
309
+ if self.audio_adapters is not None:
310
+ layers = self._find_decoder_layers(full.language_model)
311
+ if len(layers) != len(self.audio_adapters):
312
+ raise RuntimeError(
313
+ f"decoder has {len(layers)} layers but the checkpoint carries "
314
+ f"{len(self.audio_adapters)} adapters")
315
+ gate = self._rt["gate"]
316
+ self._rt["adapter_handles"] = [
317
+ layer.register_forward_hook(_make_hook(ad, gate))
318
+ for layer, ad in zip(layers, self.audio_adapters)
319
+ ]
320
+
321
+ def _load_audio_encoder(self, audio_cfg, dtype):
322
+ """Instantiate the Omni audio encoder and load only ``thinker.audio_tower.*``."""
323
+ import glob
324
+
325
+ from huggingface_hub import snapshot_download
326
+ from safetensors.torch import load_file
327
+ from transformers.models.qwen2_5_omni import modeling_qwen2_5_omni as mod
328
+
329
+ snap = snapshot_download(self.config.audio_model,
330
+ allow_patterns=["*.safetensors", "*.json"])
331
+ encoder = mod.Qwen2_5OmniAudioEncoder(audio_cfg)
332
+ prefix = "thinker.audio_tower."
333
+ collected = {}
334
+ for shard in sorted(glob.glob(os.path.join(snap, "*.safetensors"))):
335
+ for k, v in load_file(shard).items():
336
+ if k.startswith(prefix):
337
+ collected[k[len(prefix):]] = v
338
+ encoder.load_state_dict(collected, strict=False)
339
+ return encoder.to(dtype).eval()
340
+
341
+ @staticmethod
342
+ def _find_decoder_layers(base_lm: nn.Module) -> nn.ModuleList:
343
+ best = None
344
+ for name, mod_ in base_lm.named_modules():
345
+ if isinstance(mod_, nn.ModuleList) and name.rsplit(".", 1)[-1] == "layers":
346
+ if best is None or len(mod_) > len(best):
347
+ best = mod_
348
+ if best is None or len(best) == 0:
349
+ raise ValueError("no decoder ModuleList named 'layers' found in the base")
350
+ return best
351
+
352
+ # ------------------------------------------------------------- internals
353
+ def _finish(self, pooled: torch.Tensor, dim: Optional[int]) -> torch.Tensor:
354
+ dim = dim or self.config.mrl_default
355
+ return mrl_truncate_normalize(pooled.float(), dim).squeeze(0).cpu()
356
+
357
+ def _encode_text_ids(self, ids_t: torch.Tensor) -> torch.Tensor:
358
+ full = self._rt["full"]
359
+ embeds = full.get_input_embeddings()(ids_t)
360
+ out = full.language_model(inputs_embeds=embeds,
361
+ attention_mask=torch.ones_like(ids_t))
362
+ hidden = out.last_hidden_state if hasattr(out, "last_hidden_state") else out[0]
363
+ return last_token_pool(hidden, torch.ones_like(ids_t))
364
+
365
+ # ------------------------------------------------------------- embedding
366
+ @torch.no_grad()
367
+ def embed_text(self, text: str, instruction: str = DEFAULT_QUERY_INSTRUCTION,
368
+ dim: Optional[int] = None) -> torch.Tensor:
369
+ self._ensure_backbones()
370
+ if self._rt["gate"].active:
371
+ raise RuntimeError("adapter gate is open during a text encode — "
372
+ "non-audio inputs must run with the gate closed")
373
+ ids = self._rt["tok"].encode(_chat(instruction, text),
374
+ add_special_tokens=False)[: self.config.max_text_tokens]
375
+ ids_t = torch.tensor([ids], device=self._device)
376
+ pooled = self._encode_text_ids(ids_t)
377
+ return self._finish(self.text_whitening(pooled), dim)
378
+
379
+ @torch.no_grad()
380
+ def embed_audio(self, audio: Union[str, "object"], sr: Optional[int] = None,
381
+ dim: Optional[int] = None) -> torch.Tensor:
382
+ import librosa
383
+ import soundfile as sf
384
+
385
+ self._ensure_backbones()
386
+ if isinstance(audio, (str, os.PathLike)):
387
+ wav, sr = sf.read(str(audio), dtype="float32")
388
+ else:
389
+ wav = audio
390
+ assert sr is not None, "pass sr= when embedding a raw array"
391
+ if getattr(wav, "ndim", 1) > 1:
392
+ wav = wav.mean(axis=1)
393
+ fe_audio = self._rt["fe_audio"]
394
+ target_sr = fe_audio.sampling_rate
395
+ if sr != target_sr:
396
+ wav = librosa.resample(wav, orig_sr=sr, target_sr=target_sr)
397
+ feats = fe_audio(wav, sampling_rate=target_sr, return_tensors="pt",
398
+ return_attention_mask=True, padding="max_length", truncation=True)
399
+ mel = feats["input_features"][0]
400
+ am = feats.get("attention_mask")
401
+ if am is not None:
402
+ mel = mel[:, : int(am[0].sum().item())]
403
+ frames, frame_mask = self._rt["tower"](
404
+ mel.unsqueeze(0).to(self._device),
405
+ torch.ones(1, mel.shape[1], dtype=torch.bool, device=self._device))
406
+ audio_tok = self.resampler(frames, frame_mask)
407
+ cfg = self.config
408
+ ids = torch.tensor([[cfg.audio_pad_id] * cfg.n_query + [cfg.eos_id]],
409
+ device=self._device)
410
+ attention_mask = torch.ones_like(ids)
411
+ full = self._rt["full"]
412
+ embeds = full.get_input_embeddings()(ids).clone()
413
+ embeds[ids == cfg.audio_pad_id] = (
414
+ audio_tok.reshape(-1, audio_tok.size(-1)).to(embeds.dtype))
415
+ with self._rt["gate"]: # adapters ON for audio (gen 2)
416
+ out = full.language_model(inputs_embeds=embeds, attention_mask=attention_mask)
417
+ hidden = out.last_hidden_state if hasattr(out, "last_hidden_state") else out[0]
418
+ pooled = last_token_pool(hidden, attention_mask)
419
+ return self._finish(pooled, dim)
420
+
421
+ @torch.no_grad()
422
+ def embed_image(self, image, dim: Optional[int] = None) -> torch.Tensor:
423
+ from PIL import Image
424
+
425
+ self._ensure_backbones()
426
+ if self._rt["gate"].active:
427
+ # The vision path runs through the same (hook-carrying) decoder layers;
428
+ # non-audio inputs must execute with the gate closed so the adapter
429
+ # branch never runs.
430
+ raise RuntimeError("adapter gate is open during an image embed — "
431
+ "non-audio inputs must run with the gate closed")
432
+ if isinstance(image, (str, os.PathLike)):
433
+ image = Image.open(str(image))
434
+ image = image.convert("RGB")
435
+ text = _chat(DOC_INSTRUCTION, "<|vision_start|><|image_pad|><|vision_end|>")
436
+ inputs = self._rt["proc"](text=[text], images=[image],
437
+ return_tensors="pt").to(self._device)
438
+ h = self._rt["full"](**inputs).last_hidden_state
439
+ pooled = last_token_pool(h, inputs["attention_mask"])
440
+ return self._finish(pooled, dim)
441
+
442
+ # ------------------------------------------------------------- batched
443
+ @torch.no_grad()
444
+ def embed_text_batch(self, texts, instruction: str = DEFAULT_QUERY_INSTRUCTION,
445
+ dim: Optional[int] = None,
446
+ max_tokens: Optional[int] = None) -> torch.Tensor:
447
+ """Batch text embedding [B, dim] (right-padded, mask-aware last-token pooling)."""
448
+ self._ensure_backbones()
449
+ if self._rt["gate"].active:
450
+ raise RuntimeError("adapter gate is open during a text encode — "
451
+ "non-audio inputs must run with the gate closed")
452
+ cfg, tok = self.config, self._rt["tok"]
453
+ max_tokens = max_tokens or cfg.max_text_tokens
454
+ seqs = [tok.encode(_chat(instruction, t), add_special_tokens=False)[:max_tokens]
455
+ for t in texts]
456
+ L = max(len(s) for s in seqs)
457
+ ids = torch.full((len(seqs), L), cfg.pad_id, dtype=torch.long, device=self._device)
458
+ mask = torch.zeros(len(seqs), L, dtype=torch.long, device=self._device)
459
+ for b, s in enumerate(seqs):
460
+ ids[b, : len(s)] = torch.tensor(s, device=self._device)
461
+ mask[b, : len(s)] = 1
462
+ full = self._rt["full"]
463
+ out = full.language_model(inputs_embeds=full.get_input_embeddings()(ids),
464
+ attention_mask=mask)
465
+ hidden = out.last_hidden_state if hasattr(out, "last_hidden_state") else out[0]
466
+ pooled = self.text_whitening(last_token_pool(hidden, mask))
467
+ return mrl_truncate_normalize(pooled.float(), dim or cfg.mrl_default).cpu()
468
+
469
+ @torch.no_grad()
470
+ def embed_audio_batch(self, wavs, sr: int, dim: Optional[int] = None) -> torch.Tensor:
471
+ """Batch audio embedding [B, dim] from raw waveform arrays at a common rate."""
472
+ import librosa
473
+ import numpy as np
474
+
475
+ self._ensure_backbones()
476
+ cfg, fe_audio = self.config, self._rt["fe_audio"]
477
+ target_sr = fe_audio.sampling_rate
478
+ prepped = []
479
+ for wav in wavs:
480
+ wav = np.asarray(wav, dtype=np.float32)
481
+ if wav.ndim > 1:
482
+ wav = wav.mean(axis=-1)
483
+ if sr != target_sr:
484
+ wav = librosa.resample(wav, orig_sr=sr, target_sr=target_sr)
485
+ prepped.append(wav)
486
+ feats = fe_audio(prepped, sampling_rate=target_sr, return_tensors="pt",
487
+ return_attention_mask=True, padding="max_length", truncation=True)
488
+ mel, am = feats["input_features"], feats.get("attention_mask")
489
+ if am is not None:
490
+ tmax = int(am.sum(dim=1).max().item())
491
+ mel, am = mel[:, :, :tmax], am[:, :tmax]
492
+ fmask = (am.bool() if am is not None
493
+ else torch.ones(mel.shape[0], mel.shape[2], dtype=torch.bool))
494
+ frames, frame_mask = self._rt["tower"](mel.to(self._device),
495
+ fmask.to(self._device))
496
+ audio_tok = self.resampler(frames, frame_mask)
497
+ ids = torch.tensor([[cfg.audio_pad_id] * cfg.n_query + [cfg.eos_id]] * mel.shape[0],
498
+ device=self._device)
499
+ attention_mask = torch.ones_like(ids)
500
+ full = self._rt["full"]
501
+ embeds = full.get_input_embeddings()(ids).clone()
502
+ embeds[ids == cfg.audio_pad_id] = (
503
+ audio_tok.reshape(-1, audio_tok.size(-1)).to(embeds.dtype))
504
+ with self._rt["gate"]:
505
+ out = full.language_model(inputs_embeds=embeds, attention_mask=attention_mask)
506
+ hidden = out.last_hidden_state if hasattr(out, "last_hidden_state") else out[0]
507
+ pooled = last_token_pool(hidden, attention_mask)
508
+ return mrl_truncate_normalize(pooled.float(), dim or cfg.mrl_default).cpu()
509
+
510
+ # ------------------------------------------------------------- read-out
511
+ @staticmethod
512
+ def center(embs: torch.Tensor) -> torch.Tensor:
513
+ """Per-modality mean-centering + renormalization (cross-modal ranking readout)."""
514
+ c = embs - embs.mean(dim=0, keepdim=True)
515
+ return F.normalize(c, dim=-1)
516
+
517
+ def forward(self, *args, **kwargs):
518
+ raise NotImplementedError(
519
+ "fusion-embedding is an embedding model: use embed_text(str), "
520
+ "embed_audio(path_or_array, sr=...), embed_image(path_or_PIL), and "
521
+ "center(embs) for cross-modal ranking.")