File size: 11,039 Bytes
63f41a6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Regenerate tests/data/qwen3_bytelevel_oracle.json.gz -- the frozen HF reference for
tests/test_tokenizer_bytelevel.py.

RUN ONLY in the isolated rollout venv (it has transformers; the align venv must never):

    CUDA_VISIBLE_DEVICES= .venv-rollout/bin/python tests/data/make_qwen3_bytelevel_oracle.py

transformers is imported HERE, as a separate process, to DUMP reference ids/decodes.
align itself never imports it (tests/test_dependency_firewall.py scans align/ and
scripts/ only; this file lives under tests/data and is data tooling, not runtime).

Corpus: all 244 minif2f + 360 MOBench formal statements, the P2-B probe completions
(first 2000 chars each), Unicode stress strings, the exact chat-templated Goedel prompts
for the first 50 minif2f problems + correction-round prompts, chat-template cases, and
id sequences for decode. Ids are stored as zlib(uint32 LE) base64 to keep the file small.
"""
import array
import base64
import gzip
import json
import os
import random
import sys
import zlib

REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
SNAP = os.environ.get(
    "GOEDEL_SNAPSHOT",
    "/apps/development/rlhf/hf_cache/hub/models--Goedel-LM--Goedel-Prover-V2-8B/snapshots/"
    "dfd02e6271a58375dfbf3ece0175277cf6b6a89a",
)
DATASETS = "/apps/development/rlhf/reference-repos/Goedel-Prover-V2/dataset"
PROBE = os.path.join(REPO, ".tmp/p2/P2-B/probe_16k_seqs256.jsonl")
OUT = os.path.join(REPO, "tests/data/qwen3_bytelevel_oracle.json.gz")

import unicodedata  # noqa: E402

from transformers import AutoTokenizer  # noqa: E402

sys.path.insert(0, REPO)
from extras.vllm_rollout import lean_prompts  # noqa: E402  (stdlib-only at import)

hf = AutoTokenizer.from_pretrained(SNAP)


def pack(ids):
    return base64.b64encode(zlib.compress(array.array("I", ids).tobytes(), 9)).decode()


def jl(path):
    with open(path) as f:
        return [json.loads(x) for x in f if x.strip()]


STRESS = [
    "", " ", "\n", "a", "Hello, world!", "hello  world", "  leading", "trailing  ", "a\n\nb\n\n\nc",
    "line1\r\nline2\rline3\n", " \n ", "\t\tx\t", "x \t\n y", "a\u00a0b\u2003c\u3000d\u2028e\u2029f\x0bg\x0ch\x85i",
    "a\x1cb\x1dc\x1ed\x1fe", "zero\u200bwidth\u200djoiner\ufeffbom",
    "2024", "x=1234567890", "3.14159", "١٢٣٤", "²³ ½ ⅷ Ⅷ ①", "1,000,000", "a1b22c333", "0x1F",
    "don't can't I'm we're they've she'll he'd it's", "DON'T I'M WE'RE THEY'VE SHE'LL HE'D IT'S", "'s 't 're 've 'm 'll 'd",
    "'ſ", "'ſt", "x'ſ", "'S'T'RE", "rock'n'roll", "'quoted'", "o'clock", "''s", "'re's",
    "∀ x ∈ ℝ, ∃ y, x < y → ⟨x, y⟩", "theorem foo (f : ℝ → ℝ) (h₀ : ∀ x, f x = 5 * x + 4) : f 1 = 9 := by",
    "ℕ ℤ ℚ ℝ ℂ ∑ ∏ ∫ √ ≤ ≥ ≠ ≈ ∈ ∉ ⊂ ⊆ ∪ ∩ ¬ ∧ ∨ ↔ ⇒ λ π", "x₁ x₂ x₃ a⁻¹ x²",
    "你好,世界!这是一个测试。", "日本語のテキスト、ひらがな、カタカナ", "한국어 텍스트", "Привет, мир! Ёж", "مرحبا بالعالم", "שלום עולם",
    "😀 😃 👨‍👩‍👧‍👦 🏳️‍🌈 👍🏽 🇮🇳", "\U0001f600\U0001f601", "emoji😀attached",
    "e\u0301", "a\u0300\u0301\u0302", "\u1100\u1161\u11a8", "\u0041\u030a vs \u00c5", "\u212b \u2126 \ufb01",
    "caf\u00e9 cafe\u0301", "\u0958 \u0915\u093c", "fi fl ㎏ ㍿",
    "<|im_start|>user\nhi<|im_end|>\n<|im_start|>assistant\n", "<think>\nreason\n</think>\n\nanswer", "<|endoftext|>",
    "<|im_end|>\u0301", "<|im_end|>e\u0301<|im_start|>", "e<|im_end|>\u0301x", "<|im_start|", "<|IM_START|>", "<think", "</think>>",
    "<think><think></think></think>", "<|im_start|><|im_end|>", "<tool_call>{}</tool_call>", "<|fim_prefix|>a<|fim_suffix|>b<|fim_middle|>",
    "<|vision_pad|><|image_pad|><|video_pad|>", "<|box_start|>x<|box_end|>", "<|file_sep|><|repo_name|>",
    "\x00\x01\x02\x7f", "a\x00b", "\ue000 private use", "\ufffe \uffff", "\U0010ffff", "\ufffd",
    "\U00001c89 new in Unicode 16", "\U00010d4a garay digit", "\U0001e5d0 ol onal", "\U0000a7ce \U0000a7cf", "\U00031350 CJK ext H",
    "\u0378 unassigned", "\U000e0001 tag", "\u180e mongolian vowel sep",
    "x" * 5000, " " * 5000, "-" * 3000, "\n" * 2000, "1" * 3000, "ab " * 2000, "=" * 100 + "\n" + "=" * 100,
    "a" + " " * 40 + "b", "a" + "\n" * 40 + " b", "\t" * 50 + "x",
    "!!!???...", "...", "a,b;c:d", "(a)[b]{c}", "a_b_c", "__init__", "foo.bar(baz)", "$x^2$", "\\frac{1}{3}Bh",
    "/-- doc -/\ntheorem t : True := trivial", "```lean4\nexample : 1 + 1 = 2 := by norm_num\n```", "  have h : x = 1 := by\n    simp\n    <;> linarith\n",
    "https://example.com/a?b=c&d=e#f", "user@example.com", "C:\\Users\\x", "a\u2028b", "x\u00a0\u00a0y", " \u00a0 ",
    "it'sx", "'tis", "I'dnt", "she'lled", "'rest", "'very", "'Sx", "'REx", "we'veto", "they'rent", "i'mnot",
    "'The 'This 'Time 'Maybe 'Mean 'Does 'REAL 'Still 'Total", "He said 'The End' and 'THIS IS IT'",
    "word\u0301s", "1\u0301", "!\u0301", " \u0301", "\u0301", "\u0301\u0301a",
]


def main():
    out = {"snapshot": os.path.basename(SNAP), "hf_transformers": __import__("transformers").__version__,
           "hf_tokenizers": __import__("tokenizers").__version__, "texts": [], "id_seqs": [], "chat": []}

    texts = []  # (group, text)
    for r in jl(os.path.join(DATASETS, "minif2f.jsonl")):
        texts.append(("minif2f", r["formal_statement"]))
    for r in jl(os.path.join(DATASETS, "MOBench.jsonl")):
        texts.append(("mobench", r["formal_statement"]))
    for r in jl(PROBE):
        texts.append(("completion", r["completion_text"][:2000]))
    for s in STRESS:
        texts.append(("stress", s))

    minif = jl(os.path.join(DATASETS, "minif2f.jsonl"))
    for r in minif[:50]:
        prompt, msgs = lean_prompts.initial_prompt(r["formal_statement"], hf)
        texts.append(("chat_prompt", prompt))
        out["chat"].append({"messages": msgs, "kwargs": {"add_generation_prompt": True}, "expected": prompt})
    # correction rounds (assistant turn replayed, template rewrites it)
    probe = jl(PROBE)
    n_corr = 0
    for r, p in zip(minif[:20], probe[:20]):
        try:
            _, m0 = lean_prompts.initial_prompt(r["formal_statement"], hf)
            prompt, msgs = lean_prompts.correction_prompt(
                r["formal_statement"], m0, "<think>\nplan\n</think>\n\n" + p["completion_text"][:1500],
                "import Mathlib\ntheorem x : True := by\n  sorry", [], hf, 1)
        except Exception as e:  # noqa: BLE001
            print("correction prompt skipped:", repr(e)[:200])
            continue
        texts.append(("chat_prompt", prompt))
        out["chat"].append({"messages": msgs, "kwargs": {"add_generation_prompt": True}, "expected": prompt})
        n_corr += 1
    print("correction prompts:", n_corr)

    for g, t in texts:
        ids = hf.encode(t, add_special_tokens=False)
        ids2 = hf(t)["input_ids"]
        assert ids == ids2, ("add_special_tokens changes ids", g, t[:40])
        dec = hf.decode(ids)
        dec_skip = hf.decode(ids, skip_special_tokens=True)
        e = {"g": g, "t": t, "ids": pack(ids), "n": len(ids)}
        nfc = None
        # decode of the ids is the NFC text segment-wise; store explicit decode only when it is not t itself
        if dec != t:
            e["dec"] = dec
        if dec_skip != dec:
            e["dec_skip"] = dec_skip
        out["texts"].append(e)

    # chat-template cases
    U = lambda c: {"role": "user", "content": c}  # noqa: E731
    A = lambda c, **k: {"role": "assistant", "content": c, **k}  # noqa: E731
    S = lambda c: {"role": "system", "content": c}  # noqa: E731
    cases = [
        [U("hi")], [S("be brief"), U("hi")], [U("hi"), A("hello")], [S("s"), U("q1"), A("a1"), U("q2")],
        [U("q1"), A("<think>\nr1\n</think>\n\na1"), U("q2")],
        [U("q1"), A("<think>\nr1\n</think>\n\na1"), U("q2"), A("<think>\nr2\n</think>\n\na2")],
        [U("q1"), A("<think>\nr1\n</think>\n\na1")],
        [U("q1"), A("a1 no think")],
        [U("q1"), A("x"), A("<think>\nr\n</think>\n\ny")],
        [U("q1"), A("<think>\nr\n</think>\n\ny"), A("<think>\nr2\n</think>\n\nz")],
        [U("q1"), A("ans", reasoning_content="RC")],
        [U("q1"), A("ans", reasoning_content="\n\nRC\n\n")],
        [U("q1"), A("ans", reasoning_content="\n\nRC\n\n"), U("q2"), A("a2", reasoning_content="R2")],
        [U("q1"), A("</think>after")], [U("q1"), A("<think>a</think>b</think>c")],
        [U("q1"), A("<think>\n\n\nspaced\n\n\n</think>\n\n\n\nbody\n\n")],
        [U("q1"), A("\n\nleading newlines")], [U("q1"), A("")], [U(""), A("")],
        [S("sys1"), S("sys2"), U("u")], [A("first is assistant"), U("u")],
        [U("<tool_response>x</tool_response>"), A("a")], [U("u1"), U("u2")],
        [S("only system")], [U("é e\u0301 😀 <|im_end|> literal")],
    ]
    for msgs in cases:
        for kw in ({}, {"add_generation_prompt": True}, {"add_generation_prompt": True, "enable_thinking": False},
                   {"add_generation_prompt": True, "enable_thinking": True}, {"enable_thinking": False}):
            exp = hf.apply_chat_template(msgs, tokenize=False, **kw)
            out["chat"].append({"messages": msgs, "kwargs": kw, "expected": exp})
            texts_ids = hf.encode(exp, add_special_tokens=False)
            out["chat"][-1]["ids"] = pack(texts_ids)

    # id sequences for decode
    rng = random.Random(1234)
    V = len(hf)
    seqs = [[i] for i in sorted(hf.get_added_vocab().values())]
    seqs += [[rng.randrange(V) for _ in range(rng.randrange(1, 48))] for _ in range(150)]
    for s in ("😀", "你好", "👨‍👩‍👧‍👦", "é", "\U0001f1ee\U0001f1f3"):
        ids = hf.encode(s, add_special_tokens=False)
        seqs += [ids, ids[:1], ids[1:], ids[:-1], ids[-1:], ids + ids[:1]]
        seqs.append(ids[:1] + [151667] + ids[1:])  # added token splitting a character
    seqs += [[151643, 9707, 151645], [9707, 151667, 15, 151668, 220, 151644, 872, 198],
             [V - 1, V, V + 5, 151935, 151936, 200000, 9707], [], [9707] * 5]
    for s in seqs:
        rec = {"ids": s}
        for skip in (False, True):
            try:
                rec["dec_skip" if skip else "dec"] = hf.decode(s, skip_special_tokens=skip)
            except Exception as e:  # noqa: BLE001
                rec["dec_skip" if skip else "dec"] = None
                rec["err"] = type(e).__name__
        out["id_seqs"].append(rec)

    out["added_vocab"] = hf.get_added_vocab()
    out["special_ids"] = sorted(hf.all_special_ids)
    out["meta"] = {"vocab_size": hf.vocab_size, "len": len(hf), "eos": hf.eos_token_id, "pad": hf.pad_token_id,
                   "bos": hf.bos_token_id}
    with gzip.open(OUT, "wt", encoding="utf-8", compresslevel=9) as f:
        json.dump(out, f, ensure_ascii=False, separators=(",", ":"))
    print("wrote", OUT, os.path.getsize(OUT), "bytes;", len(out["texts"]), "texts,", len(out["chat"]), "chat,",
          len(out["id_seqs"]), "id seqs; tokens:", sum(e["n"] for e in out["texts"]))


if __name__ == "__main__":
    main()