streamembed 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,170 @@
1
+ """LLM part: token sequence, exact prefix reuse (with KV Reuse (ViT)) and KV Reuse (LLM).
2
+
3
+ Sequence = [prompt head (first image token at `first`)] + [one visual token per kept block] + [text tail incl. the final token].
4
+ The embedding is the normalised final hidden state of the last token.
5
+
6
+ Exact prefix reuse (on with KV Reuse): the LLM is causal, so positions before the first changed position P are bit-identical to the
7
+ previous frame. Crop the previous frame's KV cache to P and prefill only [P, L). If nothing changed, return the previous embedding.
8
+
9
+ KV Reuse (LLM) (approximate): recompute only R = visual tokens of changed blocks + every text token after the first change + the final token
10
+ (+ any slot without cached K/V). Unchanged visual tokens after the first change reuse their cached per-layer K/V although their causal
11
+ context changed: this is the approximation. Text after the change is always recomputed because it summarises the image.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import numpy as np
16
+ import torch
17
+ import torch.nn.functional as F
18
+ from transformers.cache_utils import DynamicCache
19
+ from transformers.models.qwen3_vl import modeling_qwen3_vl as MQ
20
+
21
+ from .model import Grid, Model
22
+ from .temporal import FramePlan, to_device
23
+
24
+
25
+ def full_grid_slots(g: Grid, kept: np.ndarray) -> np.ndarray:
26
+ """Full-grid slot of each position of the (pruned) sequence."""
27
+ return np.concatenate([np.arange(g.first), g.first + kept, np.arange(g.last + 1, g.length)])
28
+
29
+
30
+ def first_changed_position(g: Grid, plan: FramePlan) -> int:
31
+ """P: length of the prefix identical to the previous frame's sequence (0 without history)."""
32
+ st = plan.state
33
+ if st is None or st.prev_kept is None:
34
+ return 0
35
+ kept, prev = plan.kept, st.prev_kept
36
+ recomputed = np.zeros(g.n_blocks, bool)
37
+ recomputed[plan.recompute] = True
38
+ n = min(len(kept), len(prev))
39
+ same = (kept[:n] == prev[:n]) & ~recomputed[kept[:n]] # same block in the same slot and not recomputed
40
+ j = int(np.argmin(same)) if not same.all() else n
41
+ if j == n and len(kept) == len(prev):
42
+ return g.llm_len(len(kept)) # nothing changed
43
+ return g.first + j
44
+
45
+
46
+ def plan_llm(g: Grid, plan: FramePlan, approx_llm: bool):
47
+ """Fill plan.prefix, plan.skip_llm and (KV Reuse (LLM)) plan.llm_rows, on the host."""
48
+ st = plan.state
49
+ L = g.llm_len(len(plan.kept))
50
+ plan.prefix = first_changed_position(g, plan)
51
+ plan.skip_llm = st is not None and plan.prefix >= L and st.prev_emb is not None
52
+ if approx_llm and not plan.skip_llm:
53
+ plan.llm_rows = recompute_rows(g, plan)
54
+
55
+
56
+ def recompute_rows(g: Grid, plan: FramePlan) -> np.ndarray:
57
+ """KV Reuse (LLM): R, the sorted positions to run through the LLM (always contains the final position L-1)."""
58
+ f, kept, P = g.first, plan.kept, plan.prefix
59
+ nk = len(kept)
60
+ L = g.llm_len(nk)
61
+ r = np.zeros(L, bool)
62
+ if plan.refresh or P == 0:
63
+ r[P:] = True
64
+ else:
65
+ changed = np.zeros(g.n_blocks, bool)
66
+ changed[plan.recompute] = True
67
+ r[f:f + nk] = changed[kept] # visual tokens of recomputed blocks
68
+ r[max(P, f + nk):] = True # text after the first change
69
+ r |= ~plan.state.llm_valid[full_grid_slots(g, kept)] # slots never computed in this stream
70
+ r[L - 1] = True
71
+ return np.flatnonzero(r)
72
+
73
+
74
+ def llm_inputs(g: Grid, kept: np.ndarray, kept_t: torch.Tensor, visual_tokens: torch.Tensor, m: Model):
75
+ """Input embeddings and mRoPE positions of the (pruned) sequence.
76
+ changed: dropped blocks are removed from the sequence; the rest keep their full-grid mRoPE positions."""
77
+ seq = torch.cat([torch.arange(g.first, device=m.device), g.first + kept_t, torch.arange(g.last + 1, g.length, device=m.device)])
78
+ x = g.token_emb[seq].clone()
79
+ x[g.first:g.first + len(kept)] = visual_tokens.to(x.dtype)
80
+ return x, g.llm_pos[:, seq]
81
+
82
+
83
+ @torch.inference_mode()
84
+ def run_exact(m: Model, g: Grid, plan: FramePlan, visual_tokens: torch.Tensor, deepstack: list) -> torch.Tensor:
85
+ """Stock HF LLM forward; with KV Reuse (ViT) it continues from the previous frame's KV cache cropped to P (exact)."""
86
+ st = plan.state
87
+ if plan.skip_llm:
88
+ return st.prev_emb
89
+ x, pos = llm_inputs(g, plan.kept, plan.kept_t, visual_tokens, m)
90
+ L, f, nk = x.shape[0], g.first, len(plan.kept)
91
+ P = plan.prefix
92
+ if st is None or st.exact_kv is None or P == 0:
93
+ kv, P = DynamicCache(config=m.tcfg), 0
94
+ else:
95
+ kv = st.exact_kv # changed: reuse the previous frame's cache, cropped to P
96
+ cur = kv.get_seq_length()
97
+ if cur > P:
98
+ kv.crop(-(cur - P))
99
+ visual = torch.zeros(L, dtype=torch.bool, device=m.device)
100
+ visual[f:f + nk] = True
101
+ n_vis_in_prefix = min(max(P - f, 0), nk)
102
+ hs = m.llm(input_ids=None, inputs_embeds=x[None, P:], position_ids=pos[:, None, P:], past_key_values=kv, use_cache=True,
103
+ visual_pos_masks=visual[None, P:],
104
+ deepstack_visual_embeds=[d[n_vis_in_prefix:].to(x.dtype) for d in deepstack]).last_hidden_state
105
+ if st is not None:
106
+ st.exact_kv = kv
107
+ return F.normalize(hs[0, -1].float(), p=2, dim=-1)
108
+
109
+
110
+ @torch.inference_mode()
111
+ def run_approx(m: Model, g: Grid, plan: FramePlan, visual_tokens: torch.Tensor, deepstack: list) -> torch.Tensor:
112
+ """Run only rows R through every LLM layer. Per layer, write their fresh K/V into the full-grid slot cache and attend causally
113
+ (by sequence index) over the cached K/V of all current positions.
114
+ changed: re-implements the Qwen3 decoder layer (same modules) with a row subset and a slot-indexed K/V cache."""
115
+ st = plan.state
116
+ if plan.skip_llm:
117
+ return st.prev_emb
118
+ tc = m.tcfg
119
+ nh, nkv, hd = tc.num_attention_heads, tc.num_key_value_heads, tc.head_dim
120
+ f, nk = g.first, len(plan.kept)
121
+ x, pos = llm_inputs(g, plan.kept, plan.kept_t, visual_tokens, m)
122
+ rows_np = plan.llm_rows
123
+ slots_np = full_grid_slots(g, plan.kept)
124
+ is_vis = (rows_np >= f) & (rows_np < f + nk)
125
+ rows, slots = to_device(rows_np, m), to_device(slots_np, m)
126
+ row_slots = to_device(slots_np[rows_np], m)
127
+ vis_rows = to_device(np.flatnonzero(is_vis), m) # rows (within R) that are visual tokens
128
+ ds_rows = to_device(rows_np[is_vis] - f, m) # their index among the kept blocks
129
+ h = x[rows]
130
+ rpos = pos[:, rows][:, None]
131
+ if rpos.shape[0] == 4: # (text, t, h, w) layout -> the 3 mRoPE rows
132
+ rpos = rpos[1:]
133
+ ds = [d[ds_rows].to(h.dtype) for d in deepstack]
134
+ cos, sin = m.llm.rotary_emb(h, rpos)
135
+ cos, sin = cos[0], sin[0]
136
+ mask = (torch.arange(len(slots_np), device=m.device)[None, :] <= rows[:, None])[None, None] # causal over sequence index
137
+ for layer_id, layer in enumerate(m.llm.layers):
138
+ at = layer.self_attn
139
+ q, k, v = KERNELS["llm_qkv"](layer, h, cos, sin, nh, nkv, hd)
140
+ st.llm_k[layer_id][row_slots] = k
141
+ st.llm_v[layer_id][row_slots] = v
142
+ kf, vf = st.llm_k[layer_id][slots], st.llm_v[layer_id][slots]
143
+ o = F.scaled_dot_product_attention(q.transpose(0, 1)[None], kf.transpose(0, 1)[None], vf.transpose(0, 1)[None],
144
+ attn_mask=mask, dropout_p=0.0, scale=at.scaling, enable_gqa=True)
145
+ h = KERNELS["llm_out"](layer, h, o, len(rows_np))
146
+ if layer_id < len(ds) and len(vis_rows): # DeepStack: add ViT features after early layers
147
+ h[vis_rows] = h[vis_rows] + ds[layer_id]
148
+ st.llm_valid[slots_np[rows_np]] = True
149
+ return F.normalize(m.llm.norm(h[-1]).float(), p=2, dim=-1) # R is sorted and ends with the final position
150
+
151
+
152
+ def _llm_qkv(layer, h, cos, sin, nh: int, nkv: int, hd: int):
153
+ """input RMSNorm -> q/k/v projections -> q/k RMSNorm -> mRoPE (the dense part before attention)."""
154
+ at = layer.self_attn
155
+ a = layer.input_layernorm(h)
156
+ q = at.q_norm(at.q_proj(a).view(-1, nh, hd))
157
+ k = at.k_norm(at.k_proj(a).view(-1, nkv, hd))
158
+ v = at.v_proj(a).view(-1, nkv, hd)
159
+ q, k = MQ.apply_rotary_pos_emb(q, k, cos, sin, unsqueeze_dim=1)
160
+ return q, k, v
161
+
162
+
163
+ def _llm_out(layer, h, o, n: int):
164
+ """o_proj + residual, then post-attention RMSNorm -> MLP + residual (the dense part after attention)."""
165
+ h = h + layer.self_attn.o_proj(o[0].transpose(0, 1).reshape(n, -1))
166
+ return h + layer.mlp(layer.post_attention_layernorm(h))
167
+
168
+
169
+ # changed: the dense per-layer parts of KV Reuse (LLM) are looked up here so cpu_compile.enable() can swap in torch.compile'd versions
170
+ KERNELS = {"llm_qkv": _llm_qkv, "llm_out": _llm_out}
@@ -0,0 +1,46 @@
1
+ """Multiply-accumulate (MAC) count of the work actually done per frame (weights and attention; norms / activations ignored).
2
+
3
+ All results are Python ints (no overflow). MAC fraction = (vit_macs + llm_macs) / full_frame_macs(m, grid).
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import numpy as np
8
+
9
+ from .model import Grid, Model
10
+
11
+
12
+ def vit_macs(m: Model, n_query: int, n_kv: int, n_merge: int) -> int:
13
+ """ViT with n_query query patches attending over n_kv patches in every layer; n_merge blocks through merger + DeepStack mergers."""
14
+ cv = m.vcfg
15
+ d, inter = cv.hidden_size, cv.intermediate_size
16
+ mh = d * cv.spatial_merge_size ** 2
17
+ per_layer = n_query * (4 * d * d + 2 * d * inter) + 2 * n_query * n_kv * d
18
+ patch_embed = n_query * cv.in_channels * cv.temporal_patch_size * cv.patch_size ** 2 * d
19
+ mergers = n_merge * (len(m.deepstack_layers) + 1) * (mh * mh + mh * cv.out_hidden_size)
20
+ return len(m.vit.blocks) * per_layer + patch_embed + mergers
21
+
22
+
23
+ def _llm_linear_per_token(m: Model) -> int:
24
+ tc = m.tcfg
25
+ H, nh, nk, hd, inter = tc.hidden_size, tc.num_attention_heads, tc.num_key_value_heads, tc.head_dim, tc.intermediate_size
26
+ return 2 * H * nh * hd + 2 * H * nk * hd + 3 * H * inter # q,o + k,v + gate,up,down
27
+
28
+
29
+ def llm_macs_suffix(m: Model, P: int, L: int) -> int:
30
+ """Exact path: prefill positions [P, L); position i attends over i+1 positions."""
31
+ tc = m.tcfg
32
+ att = 2 * tc.num_attention_heads * tc.head_dim * (L * (L + 1) - P * (P + 1)) // 2
33
+ return tc.num_hidden_layers * ((L - P) * _llm_linear_per_token(m) + att)
34
+
35
+
36
+ def llm_macs_rows(m: Model, rows: np.ndarray) -> int:
37
+ """KV Reuse (LLM) path: only positions `rows` (the host indices that were executed); row i attends over i+1 positions."""
38
+ tc = m.tcfg
39
+ rows = np.asarray(rows, dtype=np.int64)
40
+ att = 2 * tc.num_attention_heads * tc.head_dim * int((rows + 1).sum())
41
+ return tc.num_hidden_layers * (len(rows) * _llm_linear_per_token(m) + att)
42
+
43
+
44
+ def full_frame_macs(m: Model, g: Grid) -> int:
45
+ """FULL config on this resolution (3.084e12 for 1280x704 / 880 blocks)."""
46
+ return vit_macs(m, g.n_patches, g.n_patches, g.n_blocks) + llm_macs_suffix(m, 0, g.length)
@@ -0,0 +1,254 @@
1
+ """Model loading (+ LoRA, optional bf16 / bf16-llm-fp32 weight storage), the linear patch embedding and per-resolution constants.
2
+
3
+ Qwen3-VL-Embedding-2B = ViT (16x16-px patches) + LLM; the embedding is the normalised final hidden state of the last token.
4
+ A 1280x704 screenshot gives 80x44 patches; the ViT merger fuses each 2x2 group of patches (= one 32x32-px "block") into one LLM token,
5
+ so 880 blocks / visual tokens. The processor already orders patches block by block (4 consecutive rows = one block).
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import warnings
10
+ from dataclasses import dataclass, field
11
+ from typing import Optional
12
+
13
+ import torch
14
+ import torch.nn.functional as F
15
+ from transformers.models.qwen3_vl import modeling_qwen3_vl as MQ
16
+
17
+ from .config import MODEL_ID
18
+ from .qwen_embedder import Qwen3VLEmbedder
19
+
20
+
21
+ @dataclass
22
+ class Model:
23
+ embedder: Qwen3VLEmbedder # processor + HF model; also the text embedder of the OCR route
24
+ device: torch.device
25
+ dtype: torch.dtype
26
+ grids: dict = field(default_factory=dict) # (grid_thw, prompt length) -> Grid
27
+
28
+ def __post_init__(self):
29
+ base = self.embedder.model
30
+ self.core = base.model # HF Qwen3VLModel
31
+ self.vit = self.core.visual
32
+ self.llm = self.core.language_model
33
+ self.vcfg, self.tcfg = self.vit.config, self.llm.config
34
+ self.image_token_id = base.config.image_token_id
35
+ self.vit_heads = self.vcfg.num_heads
36
+ self.vit_head_dim = self.vcfg.hidden_size // self.vcfg.num_heads
37
+ self.deepstack_layers = list(self.vit.deepstack_visual_indexes) # ViT layers whose output is added to early LLM layers
38
+ pe = self.vit.patch_embed
39
+ # changed: Conv3d (kernel == stride == patch) flattened once into a matrix for F.linear (see embed_patches)
40
+ self.patch_w = pe.proj.weight.detach().reshape(pe.embed_dim, -1)
41
+ self.patch_b = pe.proj.bias.detach()
42
+
43
+ def sync(self):
44
+ """Wait for the device so wall-clock timings cover the device work."""
45
+ if self.device.type == "mps":
46
+ torch.mps.synchronize()
47
+ elif self.device.type == "cuda":
48
+ torch.cuda.synchronize(self.device)
49
+
50
+
51
+ def pick_device(name: str = "auto") -> str:
52
+ if name != "auto":
53
+ return name
54
+ if torch.cuda.is_available():
55
+ return "cuda"
56
+ return "mps" if torch.backends.mps.is_available() else "cpu"
57
+
58
+
59
+ def set_mps_memory_cap(cap_gb: float) -> float:
60
+ """Cap this process's MPS allocations (useful on a shared 16 GB Mac). Returns the fraction set."""
61
+ frac = min(1.0, cap_gb * 2 ** 30 / torch.mps.recommended_max_memory())
62
+ torch.mps.set_per_process_memory_fraction(frac)
63
+ return frac
64
+
65
+
66
+ WEIGHT_STORAGE = ("native", "bf16", "bf16-llm-fp32")
67
+
68
+
69
+ def load_model(device: str = "auto", dtype: torch.dtype = torch.float16, lora: Optional[str] = None, weight_storage: str = "native",
70
+ model_id: str = MODEL_ID, lora_merge: bool = True) -> Model:
71
+ """Load Qwen3-VL-Embedding-2B on the CPU, add an optional LoRA, then move it to `device`.
72
+
73
+ lora: PEFT LoRA state dict (.pt); None = zero-shot.
74
+ lora_merge=True: W += B @ A at load, so the compute graph equals the base model (paper default, fp16 / fp32).
75
+ lora_merge=False: keep the low-rank path separate, x W^T + (x A^T) B^T, as PEFT computes it (fp32 adapter path). Use this in bf16:
76
+ rounding W + B @ A to bf16 (8-bit mantissa) loses much of the low-rank update.
77
+ weight_storage="bf16" (CPU + float32 only): keep Linear weights in the checkpoint's bf16, compute in fp32 (store_linear_weights_bf16).
78
+ weight_storage="bf16-llm-fp32": as "bf16", then the LLM decoder-layer weights are upcast to fp32 once (ViT and the input-embedding
79
+ table stay bf16): no per-call upcast in the LLM, bitwise identical outputs, ~7 GB instead of ~4.3 GB.
80
+ """
81
+ assert weight_storage in WEIGHT_STORAGE, weight_storage
82
+ device = pick_device(device)
83
+ bf16_store = weight_storage in ("bf16", "bf16-llm-fp32")
84
+ if bf16_store:
85
+ assert device == "cpu" and dtype == torch.float32, "bf16 weight storage is the CPU fp32 memory saver"
86
+ embedder = Qwen3VLEmbedder(model_id, dtype=torch.bfloat16 if bf16_store else dtype)
87
+ if lora and lora_merge and embedder.model.dtype == torch.bfloat16:
88
+ warnings.warn("merging the LoRA into bf16 weights rounds away part of the update; consider lora_merge=False (--no-lora-merge)")
89
+ if lora:
90
+ (merge_lora if lora_merge else attach_lora)(embedder.model, lora)
91
+ embedder.model.to(device).eval()
92
+ if bf16_store:
93
+ store_linear_weights_bf16(embedder.model)
94
+ if weight_storage == "bf16-llm-fp32": # changed: LLM weights upcast once at load (no per-call bf16 -> fp32 copy)
95
+ for p in embedder.model.model.language_model.layers.parameters():
96
+ p.data = p.data.float()
97
+ return Model(embedder=embedder, device=torch.device(device), dtype=dtype)
98
+
99
+
100
+ def merge_lora(model: torch.nn.Module, path: str, scale: float = 1.0) -> int:
101
+ """Merge a PEFT LoRA state dict (alpha / r = 1 for the paper's r 32 / alpha 32) into the base weights: W += scale * B @ A in fp32.
102
+
103
+ Keys look like base_model.model.<module path>.lora_A.weight; the module path is relative to Qwen3VLForEmbedding.
104
+ """
105
+ pairs = _lora_pairs(path)
106
+ for module, a, b in pairs:
107
+ w = model.get_submodule(module).weight
108
+ w.data = (w.data.float() + scale * (b.float() @ a.float())).to(w.dtype)
109
+ return len(pairs)
110
+
111
+
112
+ def _lora_pairs(path: str):
113
+ """(module path, A, B) per LoRA module of a PEFT state dict (keys base_model.model.<module path>.lora_A.weight), .safetensors or .pt."""
114
+ if str(path).endswith(".safetensors"):
115
+ from safetensors.torch import load_file
116
+ state = load_file(str(path), device="cpu")
117
+ else:
118
+ state = torch.load(path, map_location="cpu", weights_only=True)
119
+ pairs = [(key[len("base_model.model."):-len(".lora_A.weight")], a, state[key.replace(".lora_A.", ".lora_B.")])
120
+ for key, a in state.items() if key.endswith(".lora_A.weight")]
121
+ assert pairs and len(pairs) == len(state) // 2, f"unexpected LoRA state dict {path}"
122
+ return pairs
123
+
124
+
125
+ class LoRALinear(torch.nn.Module):
126
+ """nn.Linear + separate low-rank update, computed as PEFT does: A, B stay fp32 (PEFT upcasts bf16 / fp16 adapters), the input is
127
+ cast to fp32 for the low-rank path, base(x) + B(A(x)) * scale is summed in fp32 and cast back to the base output dtype.
128
+ dropout (training only, PEFT's lora_dropout) acts on the fp32 input of the low-rank path; it is the identity in eval mode."""
129
+
130
+ def __init__(self, base: torch.nn.Linear, a: torch.Tensor, b: torch.Tensor, scale: float = 1.0, dropout: float = 0.0):
131
+ super().__init__()
132
+ self.base, self.scale = base, scale
133
+ self.lora_a = torch.nn.Parameter(a.float(), requires_grad=False)
134
+ self.lora_b = torch.nn.Parameter(b.float(), requires_grad=False)
135
+ self.dropout = torch.nn.Dropout(dropout) if dropout else torch.nn.Identity()
136
+
137
+ @property
138
+ def weight(self): # expose the base weight (dtype / shape lookups by callers)
139
+ return self.base.weight
140
+
141
+ def forward(self, x):
142
+ y = self.base(x)
143
+ return (y + F.linear(F.linear(self.dropout(x.to(self.lora_a.dtype)), self.lora_a), self.lora_b) * self.scale).to(y.dtype)
144
+
145
+
146
+ def attach_lora(model: torch.nn.Module, path: str, scale: float = 1.0, dropout: float = 0.0) -> int:
147
+ """Unmerged LoRA: replace each target nn.Linear by a LoRALinear (alpha / r = 1 for the paper's r 32 / alpha 32)."""
148
+ pairs = _lora_pairs(path)
149
+ for module, a, b in pairs:
150
+ parent, _, name = module.rpartition(".")
151
+ setattr(model.get_submodule(parent), name, LoRALinear(model.get_submodule(module), a, b, scale, dropout))
152
+ return len(pairs)
153
+
154
+
155
+ def lora_state(model: torch.nn.Module) -> dict:
156
+ """PEFT-format state dict (base_model.model.<module path>.lora_A / lora_B.weight) of the attached LoRALinear modules: the file
157
+ format every --lora option reads."""
158
+ state = {}
159
+ for name, mod in model.named_modules():
160
+ if isinstance(mod, LoRALinear):
161
+ state[f"base_model.model.{name}.lora_A.weight"] = mod.lora_a.detach().cpu().clone()
162
+ state[f"base_model.model.{name}.lora_B.weight"] = mod.lora_b.detach().cpu().clone()
163
+ return state
164
+
165
+
166
+ def store_linear_weights_bf16(model: torch.nn.Module) -> None:
167
+ """CPU memory saver: nn.Linear weights (and the token-embedding table) stay bf16 and are upcast per call; everything else is fp32.
168
+
169
+ bf16 -> fp32 is exact, so outputs equal an fp32 model loaded from the bf16 checkpoint, at ~4.3 GB instead of ~8.5 GB resident.
170
+ changed: patches nn.Linear / nn.Embedding forward for the whole process (the upcast is inside every timed forward).
171
+ """
172
+ nn = torch.nn
173
+ for mod in model.modules():
174
+ keep_bf16 = isinstance(mod, nn.Linear) or (isinstance(mod, nn.Embedding) and mod.weight.shape[0] > 100000)
175
+ for name, p in mod.named_parameters(recurse=False):
176
+ if p.is_floating_point() and not (keep_bf16 and name == "weight"):
177
+ p.data = p.data.float()
178
+ for name, b in mod.named_buffers(recurse=False):
179
+ if b.is_floating_point():
180
+ setattr(mod, name, b.float())
181
+ embedding_forward = nn.Embedding.forward
182
+
183
+ def linear_upcast(self, x):
184
+ w = self.weight
185
+ return F.linear(x, w if w.dtype == x.dtype else w.to(x.dtype), self.bias)
186
+
187
+ nn.Linear.forward = linear_upcast
188
+ nn.Embedding.forward = lambda self, x: embedding_forward(self, x).float()
189
+
190
+
191
+ def embed_patches(m: Model, pixels: torch.Tensor, linear: bool) -> torch.Tensor:
192
+ """ViT patch embedding of flattened patches (n, 3*2*16*16)."""
193
+ if linear:
194
+ # changed: Conv3d patch embed -> F.linear (identical math; MPS Conv3d runs at ~0.02 TFLOP/s)
195
+ return F.linear(pixels.reshape(-1, m.patch_w.shape[1]).to(m.patch_w.dtype), m.patch_w, m.patch_b)
196
+ pe = m.vit.patch_embed
197
+ return type(pe).forward(pe, pixels) # stock HF Conv3d forward
198
+
199
+
200
+ @dataclass
201
+ class Grid:
202
+ """Constants of one (resolution, prompt) pair; identical for every frame of a stream, so computed once and cached."""
203
+ key: tuple
204
+ input_ids: torch.Tensor
205
+ vit_pos: torch.Tensor # learned ViT position embedding per patch
206
+ vit_cos: torch.Tensor # ViT 2-D RoPE per patch
207
+ vit_sin: torch.Tensor
208
+ token_emb: torch.Tensor # (L_full, H) LLM input embeddings of the prompt (image slots are overwritten by ViT features)
209
+ llm_pos: torch.Tensor # (rows, L_full) mRoPE position ids of the full-grid prompt
210
+ first: int # index of the first / last image token in the full-grid prompt
211
+ last: int
212
+ n_blocks: int # 880 for 1280x704
213
+ n_patches: int # 4 * n_blocks
214
+ length: int # L_full = prompt length with all blocks
215
+
216
+ @property
217
+ def text_tail(self) -> int:
218
+ """Prompt tokens after the image (instruction end + final token)."""
219
+ return self.length - self.last - 1
220
+
221
+ def llm_len(self, n_kept: int) -> int:
222
+ """LLM sequence length when only n_kept blocks are fed (Token Pruning drops the rest)."""
223
+ return self.first + n_kept + self.text_tail
224
+
225
+
226
+ @torch.inference_mode()
227
+ def grid(m: Model, inputs: dict) -> Grid:
228
+ """Position embeddings / RoPE / prompt embeddings, computed once per resolution.
229
+ changed: hoisted out of the per-frame forward (stock HF recomputes them every call)."""
230
+ thw, ids = inputs["image_grid_thw"], inputs["input_ids"]
231
+ key = (tuple(thw[0].tolist()), ids.shape[1])
232
+ g = m.grids.get(key)
233
+ if g is not None:
234
+ assert torch.equal(g.input_ids, ids), "same resolution and prompt length must mean the same prompt"
235
+ return g
236
+ V, cv = m.vit, m.vcfg
237
+ ii, iw = MQ.get_vision_interpolation_indices_and_weights(thw, num_grid_per_side=V.num_grid_per_side, mode=V.interpolation_mode,
238
+ align_corners=V.interpolation_align_corners,
239
+ spatial_merge_size=cv.spatial_merge_size, kwargs={})
240
+ pid = MQ.get_vision_position_ids(thw, V.spatial_merge_size, kwargs={})
241
+ pos = (V.pos_embed(ii) * iw[:, :, None]).sum(1)
242
+ cos, sin = V.rotary_pos_emb(pos, pid)
243
+ mm = inputs.get("mm_token_type_ids", (ids == m.image_token_id).int())
244
+ x0 = m.core.get_input_embeddings()(ids)
245
+ m.core.rope_deltas = None
246
+ p3 = m.core.compute_3d_position_ids(input_ids=ids, inputs_embeds=x0, image_grid_thw=thw, attention_mask=inputs["attention_mask"],
247
+ mm_token_type_ids=mm)
248
+ ipos = (ids[0] == m.image_token_id).nonzero()[:, 0]
249
+ first, last, n = int(ipos[0]), int(ipos[-1]), len(ipos)
250
+ assert last - first + 1 == n and int(thw.prod()) == 4 * n, "expected one contiguous run of image tokens, 4 patches each"
251
+ g = Grid(key=key, input_ids=ids, vit_pos=pos, vit_cos=cos, vit_sin=sin, token_emb=x0[0], llm_pos=p3[:, 0],
252
+ first=first, last=last, n_blocks=n, n_patches=4 * n, length=ids.shape[1])
253
+ m.grids[key] = g
254
+ return g
@@ -0,0 +1,106 @@
1
+ """Minimal Qwen3-VL-Embedding wrapper: processor + model + last-token pooling, images and text only.
2
+
3
+ Trimmed from Qwen's reference `qwen3_vl_embedding.py` (Apache-2.0), which ships with Qwen3-VL-Embedding. Kept identical where it matters
4
+ for the vectors: chat formatting, pixel limits, `process_vision_info(image_patch_size=16)`, processor call arguments, last-token pooling.
5
+ Removed: video inputs, token truncation helper, the automatic move to CUDA in __init__ (the caller picks the device).
6
+ The module tree is unchanged (`Qwen3VLForEmbedding.model` = HF `Qwen3VLModel`), so LoRA state-dict keys of the reference resolve as is.
7
+ """
8
+ import unicodedata
9
+ from typing import Any, Dict, List, Optional
10
+
11
+ import torch
12
+ import torch.nn.functional as F
13
+ from qwen_vl_utils.vision_process import process_vision_info
14
+ from transformers.models.qwen3_vl.modeling_qwen3_vl import Qwen3VLConfig, Qwen3VLModel, Qwen3VLPreTrainedModel
15
+ from transformers.models.qwen3_vl.processing_qwen3_vl import Qwen3VLProcessor
16
+
17
+ MAX_LENGTH = 8192
18
+ IMAGE_FACTOR = 32
19
+ MIN_PIXELS = 4 * IMAGE_FACTOR * IMAGE_FACTOR
20
+ MAX_PIXELS = 1800 * IMAGE_FACTOR * IMAGE_FACTOR
21
+ DEFAULT_INSTRUCTION = "Represent the user's input."
22
+
23
+
24
+ class Qwen3VLForEmbedding(Qwen3VLPreTrainedModel):
25
+ """Qwen3VLModel without an LM head; the embedding is the final hidden state of the last token."""
26
+ _checkpoint_conversion_mapping = {} # (no `from __future__ import annotations` here: transformers reads `config`'s class)
27
+ accepts_loss_kwargs = False
28
+ config: Qwen3VLConfig
29
+
30
+ def __init__(self, config):
31
+ super().__init__(config)
32
+ self.model = Qwen3VLModel(config)
33
+ self.post_init()
34
+
35
+ def get_input_embeddings(self):
36
+ return self.model.get_input_embeddings()
37
+
38
+ def set_input_embeddings(self, value):
39
+ self.model.set_input_embeddings(value)
40
+
41
+ def forward(self, **inputs):
42
+ return self.model(**inputs)
43
+
44
+
45
+ class Qwen3VLEmbedder:
46
+ def __init__(self, model_name_or_path: str, dtype: torch.dtype = torch.float32, max_length: int = MAX_LENGTH, local_files_only: bool = False,
47
+ min_pixels: int = MIN_PIXELS, max_pixels: int = MAX_PIXELS, revision: str | None = None):
48
+ self.max_length, self.min_pixels, self.max_pixels = max_length, min_pixels, max_pixels
49
+ self.model = Qwen3VLForEmbedding.from_pretrained(model_name_or_path, dtype=dtype, local_files_only=local_files_only,
50
+ revision=revision).eval()
51
+ self.processor = Qwen3VLProcessor.from_pretrained(model_name_or_path, padding_side="right", local_files_only=local_files_only,
52
+ revision=revision)
53
+
54
+ def without_model(self) -> "Qwen3VLEmbedder":
55
+ """Processor-only shallow copy (cheap to send to DataLoader workers started with spawn, e.g. on macOS)."""
56
+ light = object.__new__(Qwen3VLEmbedder)
57
+ light.__dict__.update({k: v for k, v in self.__dict__.items() if k != "model"})
58
+ return light
59
+
60
+ def format_model_input(self, text: Optional[str] = None, image: Optional[str] = None, instruction: Optional[str] = None) -> list:
61
+ """One chat conversation: system = instruction, user = [image] + [text]."""
62
+ if instruction:
63
+ instruction = instruction.strip()
64
+ if instruction and not unicodedata.category(instruction[-1]).startswith("P"):
65
+ instruction += "."
66
+ content = []
67
+ if image is not None:
68
+ content.append({"type": "image", "image": image if image.startswith(("http://", "https://")) else "file://" + image,
69
+ "min_pixels": self.min_pixels, "max_pixels": self.max_pixels})
70
+ if text is not None:
71
+ content.append({"type": "text", "text": text})
72
+ if not content:
73
+ content.append({"type": "text", "text": "NULL"})
74
+ return [{"role": "system", "content": [{"type": "text", "text": instruction or DEFAULT_INSTRUCTION}]},
75
+ {"role": "user", "content": content}]
76
+
77
+ def preprocess(self, conversations: List[list]) -> dict:
78
+ """Conversations -> processor tensors (CPU)."""
79
+ text = self.processor.apply_chat_template(conversations, add_generation_prompt=True, tokenize=False)
80
+ images, _, video_kwargs = process_vision_info(conversations, image_patch_size=16, return_video_metadata=True,
81
+ return_video_kwargs=True)
82
+ return self.processor(text=text, images=images, videos=None, video_metadata=None, truncation=True, max_length=self.max_length,
83
+ padding=True, do_resize=False, return_tensors="pt", **video_kwargs)
84
+
85
+ @staticmethod
86
+ def pool_last(hidden: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
87
+ """Hidden state of the last non-padding token of each row (right padding)."""
88
+ col = attention_mask.shape[1] - attention_mask.flip(dims=[1]).argmax(dim=1) - 1
89
+ return hidden[torch.arange(hidden.shape[0], device=hidden.device), col]
90
+
91
+ @torch.inference_mode()
92
+ def embed(self, conversations: List[list], normalize: bool = True) -> torch.Tensor:
93
+ """Stock (unmodified HF) forward of a batch of conversations -> (B, 2048) embeddings."""
94
+ device = next(self.model.parameters()).device
95
+ inputs = {k: v.to(device) for k, v in self.preprocess(conversations).items()}
96
+ hidden = self.model.model(**inputs).last_hidden_state
97
+ e = self.pool_last(hidden, inputs["attention_mask"])
98
+ return F.normalize(e, p=2, dim=-1) if normalize else e
99
+
100
+ def embed_texts(self, texts: List[str], instruction: Optional[str] = None) -> torch.Tensor:
101
+ return self.embed([self.format_model_input(text=t, instruction=instruction) for t in texts])
102
+
103
+ def process(self, items: List[Dict[str, Any]]) -> torch.Tensor:
104
+ """Compatibility with the reference API: items = [{"text"|"image"|"instruction": ...}]."""
105
+ return self.embed([self.format_model_input(text=i.get("text"), image=i.get("image"), instruction=i.get("instruction"))
106
+ for i in items])