abtonmoy commited on
Commit
e71f1b7
·
verified ·
1 Parent(s): 34b64b4

fusion-embedding-2-k3-vision v0.1-preview: project Kimi K3's MoonViT-V2 vision encoder into the fusion-embedding-2 space

Browse files
Files changed (5) hide show
  1. README.md +96 -0
  2. config.json +21 -0
  3. inference.py +175 -0
  4. k3vit2fe2_proj.pt +3 -0
  5. results.json +14 -0
README.md ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: cc-by-nc-4.0
3
+ base_model: moonshotai/Kimi-K3
4
+ base_model_relation: adapter
5
+ library_name: fusion-embedding
6
+ pipeline_tag: feature-extraction
7
+ tags:
8
+ - multimodal
9
+ - embeddings
10
+ - retrieval
11
+ - vision
12
+ - kimi-k3
13
+ - moonvit
14
+ ---
15
+
16
+ # fusion-embedding-2-k3-vision
17
+
18
+ Kimi K3's vision encoder, made language-searchable.
19
+
20
+ This is a small trained projector that maps Kimi K3's frozen vision encoder
21
+ (**MoonViT-V2**, about 401M parameters) into the frozen
22
+ [fusion-embedding-2](https://huggingface.co/EximiusLabs/fusion-embedding-2-2b-preview) text
23
+ space. K3's own visual features become searchable in plain language, and land in the same
24
+ space as fusion-embedding's text, image, video, and audio, plus its thermal (Ember) and
25
+ motion (Tremor) sensor packs.
26
+
27
+ It builds on **only the ~401M vision tower of Kimi K3, never the 2.8T language model**. The
28
+ tower runs on a single GPU. The projector is the only trained weight, about 3.15M
29
+ parameters.
30
+
31
+ ## Results
32
+
33
+ Text-to-image retrieval on Flickr30k (test), 1000-image gallery, querying in the
34
+ fusion-embedding-2 text space:
35
+
36
+ | Metric | fusion-embedding-2-k3-vision | same recipe, DINOv2 backbone |
37
+ | --- | --- | --- |
38
+ | R@1 | 0.432 | |
39
+ | R@5 | 0.740 | |
40
+ | **R@10** | **0.836** | 0.749 |
41
+
42
+ MoonViT-V2 was trained alongside a language model, so its features align to text more
43
+ readily than a self-supervised vision backbone. On the identical projector recipe it beats
44
+ the DINOv2 version by nearly 9 points at R@10.
45
+
46
+ ## Usage
47
+
48
+ The projector ships here; the vision tower is pulled from `moonshotai/Kimi-K3` on first use
49
+ (only the `vision_tower.*` tensors, never the 2.8T language model).
50
+
51
+ ```python
52
+ from inference import K3VisionEmbedder
53
+ m = K3VisionEmbedder.from_pretrained("EximiusLabs/fusion-embedding-2-k3-vision")
54
+ v = m.embed_image("photo.jpg") # 2048-d, in the fusion-embedding-2 text space
55
+ ```
56
+
57
+ Query it with text using fusion-embedding-2 itself:
58
+
59
+ ```python
60
+ from inference import FusionEmbedder # from the fusion-embedding-2 repo
61
+ fe = FusionEmbedder.from_pretrained("EximiusLabs/fusion-embedding-2-2b-preview")
62
+ q = fe.embed_text("a dog on a beach")
63
+ score = float(q @ v) # cosine, both unit-norm
64
+ ```
65
+
66
+ ## How it works
67
+
68
+ 1. Kimi K3's MoonViT-V2 vision tower (frozen) encodes an image into patch tokens (1024-d).
69
+ 2. The tokens are pooled into one image descriptor.
70
+ 3. A trained projector (`LayerNorm -> Linear -> GELU -> Dropout -> Linear`) maps that
71
+ descriptor into the 2048-d fusion-embedding-2 text space.
72
+ 4. Trained with image-caption InfoNCE on Flickr30k. Same recipe as the Fusion Perception
73
+ DINO to fusion-embedding projector, with K3's vision tower as the backbone.
74
+
75
+ ## What this is and is not
76
+
77
+ - It is a projector on a frozen vision encoder, not a new foundation model, and not a
78
+ fine-tune of Kimi K3's language model.
79
+ - The `base_model` tag points at `moonshotai/Kimi-K3` because the vision tower is a component
80
+ of that model. We use only MoonViT-V2, not the 2.8T language model.
81
+ - It is a research preview.
82
+
83
+ ## License and attribution
84
+
85
+ The projector weights and code in this repository are released under **CC-BY-NC-4.0**.
86
+
87
+ The vision tower is part of **Kimi K3** (Moonshot AI), used under the **Kimi K3 License**,
88
+ which permits derivative works with its notice retained. This model is a derivative that
89
+ uses K3's frozen MoonViT-V2 vision encoder. Copyright 2026 Moonshot AI.
90
+
91
+ ## Family
92
+
93
+ - [fusion-embedding-2](https://huggingface.co/EximiusLabs/fusion-embedding-2-2b-preview) (the base multimodal embedding model)
94
+ - [Ember](https://huggingface.co/EximiusLabs/fusion-embedding-2-ember) (thermal) and [Tremor](https://huggingface.co/EximiusLabs/fusion-embedding-2-tremor) (motion) sensor packs
95
+ - Cookbook: [give a Kimi K3 agent a multimodal memory](https://github.com/Eximius-Labs/cookbook)
96
+ - Code and paper: [github.com/Eximius-Labs](https://github.com/Eximius-Labs), [arXiv:2607.18666](https://arxiv.org/abs/2607.18666)
config.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "fusion-embedding-2-k3-vision",
3
+ "description": "A trained projector mapping Kimi K3's frozen MoonViT-V2 vision encoder into the fusion-embedding-2 text space, so K3's visual features become language-searchable in the shared multimodal space.",
4
+ "backbone": "moonshotai/Kimi-K3",
5
+ "backbone_component": "vision_tower (MoonViT-V2, ~401M params)",
6
+ "base_text_model": "Qwen/Qwen3-VL-Embedding-2B",
7
+ "fusion_embedding_model": "EximiusLabs/fusion-embedding-2-2b-preview",
8
+ "projector": {
9
+ "in_dim": 1024,
10
+ "out_dim": 2048,
11
+ "arch": "LayerNorm(1024)->Linear(1024,1024)->GELU->Dropout(0.2)->Linear(1024,2048)",
12
+ "params": 3150848,
13
+ "readout": "mean over merged patch tokens and 2x2 subpatches",
14
+ "weights_file": "k3vit2fe2_proj.pt"
15
+ },
16
+ "vision_attn_implementation": "eager",
17
+ "trained_on": "nlphuji/flickr30k",
18
+ "training": {"objective": "image-caption InfoNCE", "temperature": 0.07, "steps": 4000, "n_train": 6000},
19
+ "license": "cc-by-nc-4.0",
20
+ "base_license": "Kimi K3 License"
21
+ }
inference.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """fusion-embedding-2-k3-vision — Kimi K3's vision encoder, made language-searchable.
2
+
3
+ A small trained projector maps Kimi K3's frozen MoonViT-V2 vision features into the
4
+ frozen fusion-embedding-2 (Qwen3-VL-Embedding-2B) text space, so K3's own visual
5
+ features become searchable in language and sit in the same space as fusion-embedding's
6
+ text, image, video, and audio. This ships the projector only; the ~401M vision tower is
7
+ pulled from moonshotai/Kimi-K3 on first use (never the 2.8T language model).
8
+
9
+ from inference import K3VisionEmbedder
10
+ m = K3VisionEmbedder.from_pretrained("EximiusLabs/fusion-embedding-2-k3-vision")
11
+ v = m.embed_image("photo.jpg") # 2048-d, in the FE2 text space
12
+
13
+ Query with text using fusion-embedding-2 itself:
14
+ from inference import FusionEmbedder # from the fusion-embedding-2 repo
15
+ fe = FusionEmbedder.from_pretrained("EximiusLabs/fusion-embedding-2-2b-preview")
16
+ q = fe.embed_text("a dog on a beach")
17
+ score = (q @ v) # cosine, both unit-norm
18
+
19
+ MoonViT-V2 is not published as a standalone checkpoint; it lives inside Kimi-K3 with a
20
+ `vision_tower.` prefix. We load only those tensors from the shard index. Loading the
21
+ vision tower's modeling code also imports K3's language stack, so we stub that import
22
+ (the vision path never uses it) rather than pulling in the LLM's kernels.
23
+
24
+ Requires: torch, transformers>=4.57, einops, pillow, safetensors, huggingface_hub.
25
+ The Kimi-K3 base is under the Kimi K3 License (permissive, notice required); this
26
+ projector is released under CC-BY-NC-4.0.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import os
32
+ import sys
33
+ import json
34
+ import types
35
+ from typing import Optional
36
+
37
+ import torch
38
+ import torch.nn as nn
39
+ import torch.nn.functional as F
40
+
41
+ K3_REPO = "moonshotai/Kimi-K3"
42
+ BASE = "Qwen/Qwen3-VL-Embedding-2B"
43
+ VPREFIX = "vision_tower."
44
+
45
+
46
+ # --------------------------------------------------------------------------- #
47
+ # MoonViT-V2 loader: only the vision tower, no language model, no fla/triton
48
+ # --------------------------------------------------------------------------- #
49
+ def _install_llm_stub() -> None:
50
+ """Stub K3's language modeling submodule so importing the ViT never pulls the LLM."""
51
+ from importlib.abc import Loader, MetaPathFinder
52
+ from importlib.machinery import ModuleSpec
53
+
54
+ class _StubLoader(Loader):
55
+ def create_module(self, spec):
56
+ m = types.ModuleType(spec.name)
57
+ m.__getattr__ = lambda name: type(name, (), {})
58
+ return m
59
+
60
+ def exec_module(self, module):
61
+ pass
62
+
63
+ class _StubFinder(MetaPathFinder):
64
+ def find_spec(self, fullname, path=None, target=None):
65
+ if fullname.endswith("modeling_kimi_linear"):
66
+ return ModuleSpec(fullname, _StubLoader())
67
+ return None
68
+
69
+ if not any(type(f).__name__ == "_StubFinder" for f in sys.meta_path):
70
+ sys.meta_path.insert(0, _StubFinder())
71
+
72
+
73
+ def load_moonvit_tower(device: str, dtype=torch.bfloat16, token: Optional[str] = None):
74
+ """Return the frozen MoonViT-V2 tower and its image processor."""
75
+ import safetensors.torch as st
76
+ from huggingface_hub import hf_hub_download
77
+ from transformers import AutoConfig, AutoImageProcessor
78
+ from transformers.dynamic_module_utils import get_class_from_dynamic_module
79
+
80
+ token = token or os.environ.get("HF_TOKEN")
81
+ cfg = AutoConfig.from_pretrained(K3_REPO, trust_remote_code=True, token=token)
82
+ vcfg = cfg.vision_config
83
+
84
+ # the LLM stack imports transformers-internal symbols this build may not expose;
85
+ # shim them (only referenced by the LLM, which the ViT never runs).
86
+ import transformers.utils.generic as _g
87
+ if not hasattr(_g, "OutputRecorder"):
88
+ _g.OutputRecorder = type("OutputRecorder", (), {"__init__": lambda self, *a, **k: None})
89
+ if not hasattr(_g, "check_model_inputs"):
90
+ _g.check_model_inputs = lambda func=None, *a, **k: (func if callable(func) else (lambda f: f))
91
+
92
+ _install_llm_stub()
93
+ MoonViT = get_class_from_dynamic_module(
94
+ "modeling_kimi_k3.MoonViT3dPretrainedModel", K3_REPO, token=token)
95
+
96
+ # KimiK3VisionConfig prefixes vision fields with vt_; alias to the standard names
97
+ # (hidden_size is the ViT width 1024, not the 7168 LLM width).
98
+ for _std, _vt in (("hidden_size", "vt_hidden_size"),
99
+ ("num_hidden_layers", "vt_num_hidden_layers"),
100
+ ("num_attention_heads", "vt_num_attention_heads"),
101
+ ("intermediate_size", "vt_intermediate_size")):
102
+ if not hasattr(vcfg, _std) and hasattr(vcfg, _vt):
103
+ setattr(vcfg, _std, getattr(vcfg, _vt))
104
+ vcfg._attn_implementation = "eager" # the ViT attn table has no sdpa path
105
+ tower = MoonViT(vcfg)
106
+
107
+ # load only vision_tower.* tensors from the shard index (never the MoE experts)
108
+ idx = json.load(open(hf_hub_download(K3_REPO, "model.safetensors.index.json", token=token)))
109
+ wmap = idx["weight_map"]
110
+ vkeys = {k: v for k, v in wmap.items() if k.startswith(VPREFIX)}
111
+ sd = {}
112
+ for sh in sorted(set(vkeys.values())):
113
+ t = st.load_file(hf_hub_download(K3_REPO, sh, token=token))
114
+ for k in vkeys:
115
+ if wmap[k] == sh:
116
+ sd[k[len(VPREFIX):]] = t[k]
117
+ miss, unexp = tower.load_state_dict(sd, strict=False)
118
+ if miss or unexp:
119
+ raise RuntimeError(f"MoonViT load mismatch: missing={len(miss)} unexpected={len(unexp)}")
120
+
121
+ for m in tower.modules():
122
+ if hasattr(m, "attn_implementation"):
123
+ m.attn_implementation = "eager"
124
+ tower = tower.to(device, dtype).eval()
125
+ for p in tower.parameters():
126
+ p.requires_grad_(False)
127
+
128
+ proc = AutoImageProcessor.from_pretrained(K3_REPO, trust_remote_code=True, token=token)
129
+ return tower, proc
130
+
131
+
132
+ def _build_projector(in_dim: int, out_dim: int) -> nn.Sequential:
133
+ return nn.Sequential(nn.LayerNorm(in_dim), nn.Linear(in_dim, 1024), nn.GELU(),
134
+ nn.Dropout(0.2), nn.Linear(1024, out_dim))
135
+
136
+
137
+ class K3VisionEmbedder:
138
+ """Frozen MoonViT-V2 + a trained projector into the fusion-embedding-2 text space."""
139
+
140
+ def __init__(self, ckpt_path: str, device: str = "cuda", dtype=torch.bfloat16):
141
+ ck = torch.load(ckpt_path, map_location="cpu", weights_only=False)
142
+ self.cfg = ck["config"]
143
+ self.device = device
144
+ self.tower, self.proc = load_moonvit_tower(device, dtype)
145
+ self.proj = _build_projector(self.cfg["in_dim"], self.cfg["out_dim"]).to(device).float()
146
+ self.proj.load_state_dict({k: v.float() for k, v in ck["proj"].items()})
147
+ self.proj.eval()
148
+
149
+ @classmethod
150
+ def from_pretrained(cls, repo_or_path: str, device: str = "cuda",
151
+ revision: Optional[str] = None, **kw) -> "K3VisionEmbedder":
152
+ if os.path.exists(repo_or_path):
153
+ path = repo_or_path
154
+ else:
155
+ from huggingface_hub import hf_hub_download
156
+ path = hf_hub_download(repo_or_path, "k3vit2fe2_proj.pt", revision=revision)
157
+ return cls(path, device=device, **kw)
158
+
159
+ @torch.no_grad()
160
+ def _tower_feat(self, image) -> torch.Tensor:
161
+ from PIL import Image
162
+ if isinstance(image, (str, os.PathLike)):
163
+ image = Image.open(str(image))
164
+ image = image.convert("RGB")
165
+ enc = self.proc.preprocess([{"type": "image", "image": image}], return_tensors="pt")
166
+ pv = enc["pixel_values"].to(self.device, torch.bfloat16)
167
+ grid = enc["grid_thws"].to(self.device)
168
+ f = self.tower(pv, grid)[0].float() # [N_merged, 4, 1024]
169
+ return F.normalize(f.mean(dim=(0, 1)), dim=-1) # [1024]
170
+
171
+ @torch.no_grad()
172
+ def embed_image(self, image) -> torch.Tensor:
173
+ """Embed an image via MoonViT-V2 into the fusion-embedding-2 text space (2048-d)."""
174
+ feat = self._tower_feat(image)
175
+ return F.normalize(self.proj(feat), dim=-1).cpu()
k3vit2fe2_proj.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:566e0192c390ac41e342a77fe6826688df00faf9dcbb152cf4de8f377213d1d2
3
+ size 6304654
results.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "flickr30k_test_t2i": {
3
+ "R@1": 0.432,
4
+ "R@5": 0.74,
5
+ "R@10": 0.836,
6
+ "gallery": 1000,
7
+ "task": "text-to-image retrieval in the fusion-embedding-2 text space"
8
+ },
9
+ "comparison": {
10
+ "dinov2_backbone_same_recipe_R@10": 0.749,
11
+ "note": "Same projector recipe trained on a frozen DINOv2 backbone instead of MoonViT-V2."
12
+ },
13
+ "training": {"n_train": 6000, "steps": 4000, "runtime_s": 505.4, "gpu": "A100-80GB"}
14
+ }