File size: 11,465 Bytes
6e668dc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
"""Long-context coding benchmark: BF16 (DynamicCache) vs INT4-KIVI (Int4KiviCache).

Measures EXECUTED pass@1 on HumanEval in two regimes, to verify the thesis
(PROBLEM.md) that INT4-KIVI pass@1 ~= BF16 pass@1 and that the parity HOLDS at
long context -- the regime KV-cache quantization actually exists for.

Regimes
-------
* short : plain HumanEval. The user message is just the function to complete,
          so the KV cache is small (~hundreds of tokens) when decoding starts.
* long  : the SAME problems, but a long, in-distribution Python-source prefix
          (concatenated transformers ``modeling_*.py``, like
          ``scripts/quant_longctx.py``) is prepended to the user message. The
          prefix is identical for BF16 and INT4-KIVI, so the comparison stays
          apples-to-apples; it only lengthens the context, it does not change
          the task. We report the actual total context length and, for
          INT4-KIVI, the cache compression ratio vs BF16.

Both modes are batch-1 greedy (do_sample=False) -- the regime Int4KiviCache
supports. We reuse ``extract_code`` and ``run_tests`` from the existing,
validated ``scripts/humaneval_bench.py`` rather than re-implementing them.

Usage
-----
    .venv/bin/python scripts/longctx_code_bench.py --n 20 --prefix-tokens 4000
Start smaller to validate the pipeline:
    .venv/bin/python scripts/longctx_code_bench.py --n 8 --prefix-tokens 4000
"""
from __future__ import annotations

import argparse
import glob
import sys
import time

import torch
from datasets import load_dataset
from transformers import AutoModelForCausalLM, AutoTokenizer, DynamicCache

ROOT = "/home/alex/poolside-hackathon-kv-quant"
sys.path.insert(0, ROOT)

# Reuse the validated extraction + execution path.
from scripts.humaneval_bench import extract_code, run_tests
from int4_kivi.hf_cache import Int4KiviCache

MODEL = "poolside/Laguna-XS.2"


# --------------------------------------------------------------------------- #
# Long in-distribution code prefix (real Python source -> in-distribution)
# --------------------------------------------------------------------------- #
def build_prefix_text(tok, target_tokens: int) -> tuple[str, int]:
    """Concatenate transformers modeling_*.py source until >= target_tokens.

    Returns (text, n_tokens). The text is plain Python source, so it is
    in-distribution for a code model and only lengthens the context.
    """
    files = sorted(
        glob.glob(
            f"{ROOT}/.venv/**/transformers/**/modeling_*.py",
            recursive=True,
        )
    )
    if not files:
        # Fall back to repo source if the venv layout differs.
        files = sorted(glob.glob(f"{ROOT}/**/*.py", recursive=True))

    texts: list[str] = []
    for f in files:
        try:
            texts.append(open(f).read())
        except OSError:
            continue
        joined = "\n\n".join(texts)
        ids = tok(joined, return_tensors="pt").input_ids[0]
        if ids.shape[0] >= target_tokens:
            # Trim to exactly target_tokens tokens for a clean, fixed prefix.
            ids = ids[:target_tokens]
            text = tok.decode(ids, skip_special_tokens=True)
            return text, int(ids.shape[0])

    # Ran out of files before reaching the target -- use whatever we have.
    joined = "\n\n".join(texts)
    ids = tok(joined, return_tensors="pt").input_ids[0]
    return joined, int(ids.shape[0])


# --------------------------------------------------------------------------- #
# Prompt construction
# --------------------------------------------------------------------------- #
def build_input_ids(tok, prompt: str, prefix_text: str | None, device):
    """Build chat-templated input_ids for one HumanEval problem.

    ``prefix_text`` (long regime) is prepended to the user message as a quoted
    reference block; it does not change the requested task.
    """
    sys_msg = (
        "You are a Python coding assistant. Complete the function below. "
        "Return a fenced ```python``` code block containing the complete "
        "function (including signature and docstring)."
    )
    if prefix_text:
        user_msg = (
            "Here is some reference Python source code for context. You do not "
            "need to use it; it is provided only as background.\n\n"
            f"```python\n{prefix_text}\n```\n\n"
            "Now, ignoring the reference above, complete this Python function:\n\n"
            f"```python\n{prompt}```"
        )
    else:
        user_msg = f"Complete this Python function:\n\n```python\n{prompt}```"

    msgs = [
        {"role": "system", "content": sys_msg},
        {"role": "user", "content": user_msg},
    ]
    input_ids = tok.apply_chat_template(
        msgs, add_generation_prompt=True, return_tensors="pt", return_dict=False,
    ).to(device)
    return input_ids


# --------------------------------------------------------------------------- #
# Generation (batch-1 greedy, identical decode for both caches)
# --------------------------------------------------------------------------- #
@torch.no_grad()
def generate(model, input_ids, max_new: int, mode: str, config):
    """Greedy-decode with either a BF16 DynamicCache or an Int4KiviCache."""
    L = input_ids.shape[1]
    if mode == "int4":
        cache = Int4KiviCache(config=config)
    else:
        cache = DynamicCache()
    out = model.generate(
        input_ids,
        max_new_tokens=max_new,
        past_key_values=cache,
        use_cache=True,
        do_sample=False,
        num_beams=1,
    )
    token_ids = out[0, L:].tolist()
    return token_ids, cache


# --------------------------------------------------------------------------- #
# One regime (short or long) over all problems
# --------------------------------------------------------------------------- #
def run_regime(model, tok, ds, args, device, regime: str, prefix_text, prefix_tok):
    label = regime.upper()
    print(f"\n{'':═<86}")
    print(f"  REGIME: {label}"
          + (f"   (prefix ~{prefix_tok} tok)" if regime == "long" else "   (plain HumanEval)"))
    print(f"{'':═<86}")
    print(f"  {'#':>3}  {'task_id':<26}  {'BF16':>5}  {'INT4':>5}  {'ctx':>6}  {'note'}")
    print(f"{'':─<86}")

    results = {"bf16": [], "int4": []}
    ctx_lens: list[int] = []
    int4_ratios: list[float] = []
    sample_pass_completion = None

    for idx, prob in enumerate(ds):
        task = prob["task_id"]
        prompt = prob["prompt"]
        tests = prob["test"]
        entry = prob["entry_point"]

        input_ids = build_input_ids(
            tok, prompt, prefix_text if regime == "long" else None, device
        )
        ctx_len = int(input_ids.shape[1])
        ctx_lens.append(ctx_len)

        row = {}
        note = ""
        for mode in ("bf16", "int4"):
            t0 = time.time()
            token_ids, cache = generate(
                model, input_ids, args.max_new, mode, model.config
            )
            dt = time.time() - t0
            response = tok.decode(token_ids, skip_special_tokens=True)
            code = extract_code(response, prompt)
            passed, err = run_tests(code, tests, entry)
            results[mode].append(passed)
            row[mode] = passed
            if mode == "int4":
                try:
                    int4_ratios.append(cache.compression_ratio_vs_bf16())
                except Exception:
                    pass
                note = f"{dt:.0f}s"
                if passed and sample_pass_completion is None:
                    sample_pass_completion = (task, ctx_len, code)

        bsym = "βœ“" if row["bf16"] else "βœ—"
        isym = "βœ“" if row["int4"] else "βœ—"
        print(f"  {idx+1:>3}  {task:<26}  {bsym:>5}  {isym:>5}  {ctx_len:>6}  {note}",
              flush=True)

    print(f"{'':─<86}")
    return results, ctx_lens, int4_ratios, sample_pass_completion


# --------------------------------------------------------------------------- #
# Summary helpers
# --------------------------------------------------------------------------- #
def summarize(regime, results, ctx_lens, int4_ratios, n):
    bf = sum(results["bf16"])
    iq = sum(results["int4"])
    agree = sum(a == b for a, b in zip(results["bf16"], results["int4"]))
    print(f"\n  [{regime.upper()}] pass@1   BF16 = {bf}/{n} ({100*bf/n:.0f}%)   "
          f"INT4-KIVI = {iq}/{n} ({100*iq/n:.0f}%)")
    print(f"  [{regime.upper()}] BF16-vs-INT4 agreement: {agree}/{n} "
          f"({100*agree/n:.0f}% same pass/fail)")
    if ctx_lens:
        print(f"  [{regime.upper()}] context length: min={min(ctx_lens)} "
              f"max={max(ctx_lens)} mean={sum(ctx_lens)//len(ctx_lens)} tokens")
    if int4_ratios:
        avg = sum(int4_ratios) / len(int4_ratios)
        print(f"  [{regime.upper()}] INT4-KIVI cache compression vs BF16: "
              f"{avg:.2f}x (mean over problems)")
    return bf, iq, agree


# --------------------------------------------------------------------------- #
# Main
# --------------------------------------------------------------------------- #
def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--n", type=int, default=20, help="number of HumanEval problems")
    ap.add_argument("--max-new", type=int, default=512, help="max new tokens")
    ap.add_argument("--prefix-tokens", type=int, default=4000,
                    help="target long-context prefix length in tokens")
    ap.add_argument("--regime", choices=["short", "long", "both"], default="both")
    args = ap.parse_args()

    print(f"[load] {MODEL} ...", flush=True)
    tok = AutoTokenizer.from_pretrained(MODEL)
    model = AutoModelForCausalLM.from_pretrained(
        MODEL, torch_dtype=torch.bfloat16, device_map="auto"
    )
    model.eval()
    device = next(model.parameters()).device

    print(f"[data] HumanEval ({args.n} problems) ...", flush=True)
    ds = load_dataset("openai/openai_humaneval", split="test").select(range(args.n))

    prefix_text, prefix_tok = None, 0
    if args.regime in ("long", "both"):
        print(f"[prefix] building ~{args.prefix_tokens}-token in-distribution "
              f"code prefix ...", flush=True)
        prefix_text, prefix_tok = build_prefix_text(tok, args.prefix_tokens)
        print(f"[prefix] actual prefix length: {prefix_tok} tokens", flush=True)

    regimes = (["short", "long"] if args.regime == "both" else [args.regime])
    summary = {}
    samples = {}
    for regime in regimes:
        res, ctx, ratios, sample = run_regime(
            model, tok, ds, args, device, regime, prefix_text, prefix_tok
        )
        summary[regime] = (res, ctx, ratios)
        samples[regime] = sample

    # ---- final table ----
    print(f"\n{'':β–ˆ<86}")
    print("  FINAL RESULTS  (executed pass@1, greedy, batch-1)")
    print(f"{'':β–ˆ<86}")
    for regime in regimes:
        res, ctx, ratios = summary[regime]
        summarize(regime, res, ctx, ratios, args.n)

    # ---- representative passing INT4-KIVI completion (prefer long) ----
    chosen = samples.get("long") or samples.get("short")
    if chosen:
        task, ctx_len, code = chosen
        print(f"\n{'':─<86}")
        print(f"  Representative PASSING INT4-KIVI completion  "
              f"(task={task}, ctx={ctx_len} tok)")
        print(f"{'':─<86}")
        print(code.rstrip()[:1600])
        print(f"{'':─<86}")

    print("\n[done]", flush=True)


if __name__ == "__main__":
    main()