File size: 9,474 Bytes
25e8424
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fb58c6c
 
25e8424
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fb58c6c
25e8424
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fb58c6c
 
 
25e8424
 
 
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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
"""On-device accuracy for the fully patched model under PyTorch Native (torch.compile(backend="neuron")).

* Accuracy: scores of the patched model vs the stock Hugging Face model on CPU in fp32 (exact GELU), on
  real-text query/passage pairs with ragged lengths. Gates: Pearson >= 0.9999, and top-1 agreement per
  query no worse than stock PyTorch Native (same tanh GELU, no kernels) on the same inputs.
* Padding isolation: the score of sequence 0 is bit-identical when its batch neighbours change.

Run inside the PyTorch Native container on trn2, once per LNC:
  NEURON_LOGICAL_NC_CONFIG=1 python -m pytest -m neuron -s tests/test_device.py
Results are also appended as JSON lines to $GTE_RESULTS (default tests/device_results.jsonl).
"""
import json
import os
import random

import numpy as np
import pytest

from conftest import load_package

torch = pytest.importorskip("torch")
pytest.importorskip("torch_neuronx")
transformers = pytest.importorskip("transformers")
pytestmark = pytest.mark.neuron

MODEL = os.environ.get("GTE_TEST_MODEL", "Alibaba-NLP/gte-multilingual-reranker-base")
RESULTS = os.environ.get("GTE_RESULTS", os.path.join(os.path.dirname(os.path.abspath(__file__)),
                                                     "device_results.jsonl"))
# (L, batch): batch 32 is the main throughput shape; batch 3 exercises the LNC2 query split.
SHAPES = [(256, 32), (128, 32), (512, 3), (1024, 8), (768, 3)]
ATTN_LENGTHS = tuple(int(v) for v in os.environ.get("GTE_ATTN_LENGTHS", "128,256,384,512,768,1024").split(","))

TOPICS = {
    "What are the benefits of renewable energy?": [
        "Solar and wind power generate electricity without burning fuel, so they emit no greenhouse gases while running.",
        "Renewable projects reduce a country's dependence on imported oil and gas and keep energy spending local.",
        "Battery storage lets grids keep using solar power after sunset, smoothing out supply.",
    ],
    "How do I reset my home wifi router?": [
        "Hold the small reset button on the back of the router for about ten seconds until the lights blink.",
        "After a factory reset, log in to the admin page with the default password printed on the label.",
        "Restarting the modem first and then the router often fixes a dropped internet connection.",
    ],
    "What are the symptoms of vitamin D deficiency?": [
        "Low vitamin D levels can cause tiredness, bone pain, muscle weakness and frequent infections.",
        "People who get little sunlight are more likely to be short of vitamin D, especially in winter.",
        "A simple blood test measures vitamin D, and supplements usually correct a deficiency within months.",
    ],
    "What is the best way to learn Python programming?": [
        "Start with small scripts that automate something you do every day, then read other people's code.",
        "The official Python tutorial covers variables, loops, functions and classes with short examples.",
        "Practice problems and a small personal project teach more than reading about syntax.",
    ],
    "How did the Roman Empire fall?": [
        "The Western Roman Empire weakened through civil wars, economic trouble and pressure on its borders.",
        "In 476 the last western emperor was deposed, an event often used to mark the end of ancient Rome.",
        "The eastern half of the empire survived for almost a thousand more years as the Byzantine Empire.",
    ],
    "How do vaccines protect against disease?": [
        "A vaccine trains the immune system to recognise a virus or bacterium without causing the illness.",
        "After vaccination, memory cells remain ready to produce antibodies quickly if the real germ appears.",
        "Some vaccines need booster doses because immune memory fades over time.",
    ],
    "How can I sleep better at night?": [
        "Going to bed and waking up at the same time every day helps set your internal clock.",
        "Avoid caffeine in the afternoon and keep screens out of the bedroom in the last hour before sleep.",
        "A cool, dark and quiet room makes it easier to fall asleep and stay asleep.",
    ],
    "What causes inflation?": [
        "Inflation rises when demand for goods and services grows faster than the economy can produce them.",
        "Higher energy and wage costs can push companies to raise prices across the economy.",
        "Central banks raise interest rates to slow borrowing and bring inflation back down.",
    ],
}


def _pairs(L, n_per_query):
    """Each query gets n_per_query passages: half on-topic, half from other topics, with varied lengths
    (one to many sentences, so real lengths spread from short to the full L)."""
    rng = random.Random(L)
    qs = list(TOPICS)
    Q, Ps = [], []
    for qi, q in enumerate(qs):
        for j in range(n_per_query):
            pool = TOPICS[q] if j % 2 == 0 else TOPICS[qs[(qi + 1 + j) % len(qs)]]
            n_sent = rng.randint(1, max(1, L // 12))
            Ps.append(" ".join(rng.choice(pool) for _ in range(n_sent)))
            Q.append(q)
    return Q, Ps


def _load(dtype):
    m = transformers.AutoModelForSequenceClassification.from_pretrained(
        MODEL, trust_remote_code=True, attn_implementation="eager").eval()
    if dtype is not None:
        for mod in m.modules():
            for cn, c in mod.named_children():
                if "GELU" in type(c).__name__:
                    setattr(mod, cn, torch.nn.GELU(approximate="tanh"))
        m = m.to(dtype)
    return m


def _rank_top1(s, ref, n_q, per):
    return float(np.mean([np.argmax(s[i * per:(i + 1) * per]) == np.argmax(ref[i * per:(i + 1) * per])
                          for i in range(n_q)]))


@pytest.fixture(scope="module")
def setup():
    import torch._dynamo as dyn
    dyn.config.cache_size_limit = 64
    pkg = load_package()
    # count trace-time calls of each fused op, so a silent fallback to the stock modules fails the test
    to = __import__("sys").modules["gte_encoder_neuron_kernels.torch_ops"]
    calls = {}
    for name in ("attn_block", "gated_mlp_ln", "res_ln", "ln"):
        def wrap(f, name=name):
            def g(*a, **k):
                calls[name] = calls.get(name, 0) + 1
                return f(*a, **k)
            return g
        setattr(to, name, wrap(getattr(to, name)))
    tok = transformers.AutoTokenizer.from_pretrained(MODEL, trust_remote_code=True)
    cpu = _load(None)
    dev = torch.device("neuron")
    stock = _load(torch.bfloat16).to(dev)
    patched = _load(torch.bfloat16)
    n = pkg.patch_model(patched, attn_lengths=ATTN_LENGTHS)
    patched = patched.to(dev)
    return dict(pkg=pkg, tok=tok, cpu=cpu, dev=dev, stock=stock, patched=patched, n_layers=n, calls=calls)


def _run(model, ids, msk, bs, dev):
    cm = torch.compile(model, backend="neuron", dynamic=False)
    out = []
    with torch.no_grad():
        for i in range(0, ids.shape[0], bs):
            out.append(cm(ids[i:i + bs].to(dev), msk[i:i + bs].to(dev)).logits[:, 0].float().cpu())
    return torch.cat(out).numpy(), cm


@pytest.mark.parametrize("L,bs", SHAPES)
def test_accuracy_and_isolation(setup, L, bs):
    tok, cpu, dev = setup["tok"], setup["cpu"], setup["dev"]
    per = 8                                       # passages per query -> 64 pairs
    Q, Ps = _pairs(L, per)
    n = len(Q) - len(Q) % bs if len(Q) % bs else len(Q)
    e = tok(Q[:n], Ps[:n], return_tensors="pt", max_length=L, padding="max_length", truncation=True)
    ids, msk = e["input_ids"], e["attention_mask"]
    n_q = n // per
    with torch.no_grad():
        ref = torch.cat([cpu(ids[i:i + 16], msk[i:i + 16]).logits[:, 0] for i in range(0, n, 16)]).float().numpy()
    s_stock, _ = _run(setup["stock"], ids, msk, bs, dev)
    setup["calls"].clear()
    s_nki, cm = _run(setup["patched"], ids, msk, bs, dev)
    traced = dict(setup["calls"])

    pr_stock = float(np.corrcoef(s_stock, ref)[0, 1])
    pr_nki = float(np.corrcoef(s_nki, ref)[0, 1])
    top1_stock = _rank_top1(s_stock, ref, n_q, per)
    top1_nki = _rank_top1(s_nki, ref, n_q, per)

    # padding isolation: keep sequence 0, replace every neighbour with other pairs of different lengths
    b0 = ids[:bs].clone(), msk[:bs].clone()
    b1 = ids[:bs].clone(), msk[:bs].clone()
    if bs > 1:
        perm = torch.arange(n - 1, n - bs, -1)
        b1[0][1:], b1[1][1:] = ids[perm], msk[perm]
    with torch.no_grad():
        y0 = cm(b0[0].to(dev), b0[1].to(dev)).logits[0].float().cpu()
        y1 = cm(b1[0].to(dev), b1[1].to(dev)).logits[0].float().cpu()
    isolated = bool(torch.equal(y0, y1))

    lens = msk.sum(-1).numpy()
    rec = dict(model=MODEL, lnc=setup["pkg"].LNC, L=L, bs=bs, pairs=n, real_len=[int(lens.min()), int(lens.max())],
               layers_patched=setup["n_layers"], pearson_stock=pr_stock, pearson_nki=pr_nki,
               top1_stock=top1_stock, top1_nki=top1_nki,
               max_abs_err_nki=float(np.abs(s_nki - ref).max()), max_abs_err_stock=float(np.abs(s_stock - ref).max()),
               padding_isolated=isolated, traced_ops=traced)
    print(json.dumps(rec), flush=True)
    with open(RESULTS, "a") as f:
        f.write(json.dumps(rec) + "\n")
    nl = setup["n_layers"]
    if L in ATTN_LENGTHS:
        assert traced.get("attn_block", 0) >= nl, rec
    assert traced.get("gated_mlp_ln", 0) >= nl and traced.get("ln", 0) >= 1, rec
    assert pr_nki >= 0.9999, rec
    assert top1_nki >= top1_stock, rec
    assert isolated, rec