File size: 6,246 Bytes
9c0a4e6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Comprehensive GPU validation for the ASD custom generation method.

Covers:
1. Native strict baseline vs ASD(B=0)        -> must be token-identical (gold check)
2. Native strict vs custom (no knobs)        -> must be token-identical (lossless default)
3. ASD(B=2.0) relaxed                        -> runs, deterministic, >= strict acceptance
4. Determinism                               -> two identical relaxed runs, token-identical
5. Error handling                            -> do_sample / ensemble_weight / no draft source
6. Throughput + acceptance comparison        -> strict vs relaxed
"""

import os
import sys
import time

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

REPO = os.environ.get("ASD_REPO", os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
TARGET = os.environ.get("ASD_TARGET", "Qwen/Qwen2.5-1.5B-Instruct")
DRAFT = os.environ.get("ASD_DRAFT", "Qwen/Qwen2.5-0.5B-Instruct")
MAX_NEW = 128
PROMPTS = [
    "Give me a short introduction to large language models.",
    "What is the capital of France? Answer in one sentence.",
    "Write a Python function that computes the nth Fibonacci number.",
    "Explain the difference between TCP and UDP briefly.",
    "List three prime numbers greater than 10 and justify one of them.",
]

failures = []


def check(name, cond, detail=""):
    status = "PASS" if cond else "FAIL"
    print(f"[{status}] {name}" + (f" -- {detail}" if detail else ""))
    if not cond:
        failures.append(name)


def count_fwd(tok, out, prompt_len):
    return out.shape[1] - prompt_len


def main():
    tokenizer = AutoTokenizer.from_pretrained(TARGET)
    model = AutoModelForCausalLM.from_pretrained(TARGET, dtype="auto", device_map="auto")
    assistant = AutoModelForCausalLM.from_pretrained(DRAFT, dtype="auto", device_map="auto")
    model.eval()
    assistant.eval()

    def run(prompt, **extra):
        inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
        t0 = time.perf_counter()
        out = model.generate(
            **inputs, max_new_tokens=MAX_NEW, do_sample=False, **extra
        )
        dt = time.perf_counter() - t0
        n = out.shape[1] - inputs["input_ids"].shape[1]
        return out, n / dt

    # ---------- 1&2. gold checks ----------
    for prompt in PROMPTS:
        strict, _ = run(prompt, assistant_model=assistant)
        b0, _ = run(
            prompt,
            assistant_model=assistant,
            custom_generate=REPO,
            trust_remote_code=True,
            assistant_asd_budget=0.0,
        )
        noknobs, _ = run(
            prompt,
            assistant_model=assistant,
            custom_generate=REPO,
            trust_remote_code=True,
        )
        check(f"gold B=0 identical ({prompt[:25]!r})", torch.equal(strict, b0))
        check(f"gold no-knobs identical ({prompt[:25]!r})", torch.equal(strict, noknobs))

    # ---------- 3&4. relaxed run + determinism ----------
    for prompt in PROMPTS:
        relaxed1, tps1 = run(
            prompt,
            assistant_model=assistant,
            custom_generate=REPO,
            trust_remote_code=True,
            assistant_asd_budget=2.0,
            assistant_asd_local_ratio=0.25,
            assistant_asd_max_mismatches=2,
        )
        relaxed2, _ = run(
            prompt,
            assistant_model=assistant,
            custom_generate=REPO,
            trust_remote_code=True,
            assistant_asd_budget=2.0,
            assistant_asd_local_ratio=0.25,
            assistant_asd_max_mismatches=2,
        )
        check(f"determinism ({prompt[:25]!r})", torch.equal(relaxed1, relaxed2))
        check(f"relaxed non-empty ({prompt[:25]!r})", relaxed1.shape[1] > 0)

    # ---------- 5. error handling ----------
    inputs = tokenizer(PROMPTS[0], return_tensors="pt").to(model.device)

    def expect_raise(name, **kw):
        try:
            model.generate(**inputs, max_new_tokens=8, **kw)
        except ValueError:
            check(f"raises {name}", True)
        except Exception as e:  # noqa: BLE001
            check(f"raises {name}", False, f"wrong exception: {type(e).__name__}: {e}")
        else:
            check(f"raises {name}", False, "no exception")

    expect_raise(
        "do_sample=True",
        assistant_model=assistant,
        custom_generate=REPO,
        trust_remote_code=True,
        do_sample=True,
        assistant_asd_budget=2.0,
    )
    expect_raise(
        "ensemble_weight conflict",
        assistant_model=assistant,
        custom_generate=REPO,
        trust_remote_code=True,
        assistant_ensemble_weight=0.5,
        assistant_asd_budget=2.0,
    )
    expect_raise(
        "knobs without draft source",
        custom_generate=REPO,
        trust_remote_code=True,
        assistant_asd_budget=2.0,
    )
    expect_raise(
        "negative budget",
        assistant_model=assistant,
        custom_generate=REPO,
        trust_remote_code=True,
        assistant_asd_budget=-1.0,
    )

    # ---------- 6. throughput + acceptance ----------
    print("\n=== throughput comparison ===")
    tot_strict_tps = tot_relaxed_tps = 0.0
    for prompt in PROMPTS:
        strict, s_tps = run(prompt, assistant_model=assistant)
        relaxed, r_tps = run(
            prompt,
            assistant_model=assistant,
            custom_generate=REPO,
            trust_remote_code=True,
            assistant_asd_budget=2.0,
            assistant_asd_local_ratio=0.25,
            assistant_asd_max_mismatches=2,
        )
        tot_strict_tps += s_tps
        tot_relaxed_tps += r_tps
        overlap = sum(
            1 for a, b in zip(strict[0].tolist(), relaxed[0].tolist()) if a == b
        ) / max(len(strict[0]), len(relaxed[0]))
        print(
            f"  strict {s_tps:6.2f} tok/s | relaxed {r_tps:6.2f} tok/s "
            f"({(r_tps / s_tps - 1) * 100:+5.1f}%) | prefix overlap {overlap:.2%}"
        )
    print(
        f"  MEAN strict {tot_strict_tps / len(PROMPTS):.2f} tok/s | "
        f"relaxed {tot_relaxed_tps / len(PROMPTS):.2f} tok/s"
    )

    print("\n" + ("ALL PASS" if not failures else f"FAILURES: {failures}"))
    sys.exit(1 if failures else 0)


if __name__ == "__main__":
    main()