File size: 3,033 Bytes
ea380ef
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python
"""Compute UEmbed (Qwen3.5) embeddings with plain `transformers`.

The released checkpoint is a standard Qwen3.5 model whose tokenizer already
appends the N=16 sparse EOS tokens, so no `trust_remote_code` and no processor
patching are needed. This script shows both dense (`last.normal`) and sparse
(`splade.last`) embeddings for text- and image-containing inputs.

Usage:
    python examples/transformers_example.py ./models/UEmbed-2B
"""

import argparse

import torch
import torch.nn.functional as F
from transformers import AutoModel, AutoProcessor
from qwen_vl_utils.vision_process import process_vision_info

NUM_EOS_TOKENS = 16  # from sparse_info.json

CONVERSATIONS = [
    [{"role": "user", "content": [
        {"type": "text", "text": "A woman playing with her dog on a beach at sunset."},
    ]}],
    [{"role": "user", "content": [
        {"type": "image", "image": "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen-VL/assets/demo.jpeg"},
        {"type": "text", "text": "A woman and her dog on the beach."},
    ]}],
]


def main():
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("model_path", nargs="?", default="./models/UEmbed-2B")
    args = parser.parse_args()

    model = AutoModel.from_pretrained(args.model_path, dtype=torch.bfloat16).eval().cuda()
    processor = AutoProcessor.from_pretrained(args.model_path, padding_side="right")

    texts = processor.apply_chat_template(CONVERSATIONS, add_generation_prompt=True, tokenize=False)
    images, videos, video_kwargs = process_vision_info(
        CONVERSATIONS, image_patch_size=16, return_video_metadata=True, return_video_kwargs=True
    )
    inputs = processor(
        text=texts, images=images, videos=videos,
        padding=True, return_tensors="pt", **(video_kwargs or {}),
    ).to(model.device)

    with torch.no_grad():
        hidden_state = model(**inputs).last_hidden_state

    # Index of the last real token per sample (before right-padding).
    last_idx = inputs.attention_mask.cumsum(dim=1).argmax(dim=1)
    rows = torch.arange(len(last_idx))

    # Dense: hidden state of the EOS right before the appended sparse tokens.
    dense = F.normalize(hidden_state[rows, last_idx - NUM_EOS_TOKENS], p=2, dim=-1)  # [B, 2048]

    # Sparse: project the N appended EOS hidden states with the per-cluster heads.
    sw = torch.load(f"{args.model_path}/sparse_weights.pt", map_location=model.device, weights_only=True)
    logits = [
        F.linear(hidden_state[rows, last_idx - (NUM_EOS_TOKENS - 1 - i)].to(head.dtype), head, bias)
        for i, (head, bias) in enumerate(zip(sw["sparse_lm_heads"], sw["sparse_bias"]))
    ]
    sparse = torch.log1p(F.relu(torch.cat(logits, dim=-1)))  # [B, 184016]

    print(f"dense:  {tuple(dense.shape)}")
    print("dense cosine matrix:\n", (dense.float() @ dense.float().T).cpu().numpy().round(4))
    print(f"sparse: {tuple(sparse.shape)}  nnz={ (sparse > 0).sum(-1).tolist() }")


if __name__ == "__main__":
    main()