File size: 5,250 Bytes
307b208
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import asyncio
import json
import shutil
from pathlib import Path

import numpy as np
import torch
from PIL import Image
from transformers import AutoModel, AutoProcessor

from coreai_embeddinggemma2 import EmbeddingGemma2CoreAI


def cases():
    wave = (0.2 * np.sin(2 * np.pi * 440 * np.arange(16000) / 16000)).astype(np.float32)
    image = Image.new("RGB", (320, 192), (20, 80, 210))
    gradient = Image.fromarray(
        np.tile(np.arange(256, dtype=np.uint8)[None, :, None], (128, 1, 3))
    )
    frames = np.stack(
        [
            np.asarray(image.resize((128, 96))),
            np.asarray(Image.new("RGB", (128, 96), (210, 50, 20))),
        ]
    )
    return [
        (
            "query",
            {"text": "What causes the northern lights?", "prompt_name": "SearchQuery"},
        ),
        (
            "document",
            {
                "text": "The northern lights are caused by charged particles from the sun.",
                "prompt_name": "Document",
            },
        ),
        ("unprefixed", {"text": "An apple a day keeps the doctor away."}),
        (
            "code",
            {"text": "def add(a, b): return a + b", "prompt_name": "CodeRetrieval"},
        ),
        ("spanish", {"text": "La biblioteca está abierta hasta las ocho."}),
        ("japanese", {"text": "桜の花が春に咲きます。"}),
        ("short", {"text": "cat"}),
        ("long", {"text": "The oceans contain diverse ecosystems. " * 90}),
        ("near-capacity", {"text": "The oceans contain diverse ecosystems. " * 160}),
        ("image", {"images": image}),
        ("gradient", {"images": gradient}),
        ("image-text", {"text": "<|image|> A blue rectangle.", "images": image}),
        ("two-images", {"images": [image, gradient]}),
        ("audio", {"audio": wave}),
        ("audio-text", {"text": "<|audio|> A musical tone.", "audio": wave}),
        ("audio-short", {"audio": wave[:3200]}),
        ("audio-long", {"audio": np.tile(wave, 5)}),
        ("audio-limit", {"audio": np.pad(np.tile(wave, 11), (0, 3200))}),
        ("video", {"videos": frames}),
        ("video-text", {"text": "<|video|> Colors changing.", "videos": frames}),
        (
            "mixed",
            {
                "text": "<|image|> Blue. <|audio|> Tone. <|video|> Colors.",
                "images": image,
                "audio": wave,
                "videos": frames,
            },
        ),
    ]


async def main():
    torch.set_num_threads(4)
    root = Path("bundles/embeddinggemma-2-CoreAI")
    source = Path("upstream/embeddinggemma-2")
    for f in source.iterdir():
        if f.is_file() and f.suffix in [".json", ".jinja", ".model"]:
            shutil.copy2(f, root / f.name)
    m = AutoModel.from_pretrained(
        source,
        dtype=torch.float32,
        attn_implementation={
            "language_model": "sdpa",
            "vision_tower": "sdpa",
            "audio_tower": "eager",
        },
    ).eval()
    p = AutoProcessor.from_pretrained(source)
    print("Loading Core AI", flush=True)
    runtime = await EmbeddingGemma2CoreAI.load(root)
    print("Loaded Core AI", flush=True)
    records = []
    for name, args in cases():
        data = dict(args)
        prompt = data.pop("prompt_name", None)
        if prompt:
            data["text"] = runtime.prompts[prompt] + data["text"]
        if "audio" in data:
            data["sampling_rate"] = 16000
        if "videos" in data:
            data["videos_kwargs"] = {"do_sample_frames": False}
        e = p(**data, return_tensors="pt")
        with torch.no_grad():
            h = m(**e).last_hidden_state
            mask = e["attention_mask"][:, :, None].float()
            reference = torch.nn.functional.normalize(
                (h * mask).sum(1) / mask.sum(1), dim=-1
            )[0].numpy()
        actual = await runtime.encode_processed({k: v.numpy() for k, v in e.items()})
        record = {
            "name": name,
            "tokens": e["input_ids"].shape[1],
            "max_abs_error": float(np.max(np.abs(actual - reference))),
            "cosine": float(actual @ reference),
            "finite": bool(np.isfinite(actual).all()),
            "unit_norm": float(np.linalg.norm(actual)),
        }
        print(record, flush=True)
        records.append(record)
        assert (
            record["finite"]
            and record["cosine"] > 0.99999
            and record["max_abs_error"] < 0.00005
        ), record
    for dim in [128, 256, 512]:
        actual = await runtime.encode("The sky is blue.", truncate_dim=dim)
        assert actual.shape == (dim,) and abs(np.linalg.norm(actual) - 1) < 1e-5
    try:
        await runtime.encode("word " * 3000)
    except ValueError:
        pass
    else:
        raise AssertionError("Over-capacity input accepted")
    (root / "validation.json").write_text(
        json.dumps(
            {
                "thresholds": {"min_cosine": 0.99999, "max_abs_error": 0.00005},
                "cases": records,
                "truncation_dims": [128, 256, 512],
                "oversized_input_rejected": True,
            },
            indent=2,
        )
        + "\n"
    )


if __name__ == "__main__":
    asyncio.run(main())