File size: 7,693 Bytes
698535a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Shared paths, the live arena definition, jsonl helpers, the Granite tokenizer and stage markers."""

from __future__ import annotations

import json
import os
import sys
import time
import urllib.request
from collections.abc import Iterable, Iterator
from functools import lru_cache
from pathlib import Path
from typing import Any

ROOT = Path(__file__).resolve().parent.parent
ARENA_ID = "ctx_compress"
ARENA_URL = f"https://api.microtensor.cloud/v2/arenas/{ARENA_ID}"
BASE = "ibm-granite/granite-4.0-h-micro"
BASE_REVISION = "d5f01a3ea75f088947be3aae039f4ad52837dfde"
PROBE_HF = "Qwen/Qwen2.5-1.5B-Instruct"
PROBE_HF_REVISION = "989aa7980e4cf806f80c7fef2b1adb7bc71aa306"
HEADINGS = (
    "Primary request and intent",
    "Key technical concepts",
    "Files and code sections",
    "Errors and fixes",
    "Problem solving",
    "All user messages",
    "Pending tasks",
    "Current work",
    "Next step",
)
# The values the project was built against; `check_definition` warns when the live arena moves.
EXPECTED = {
    "allowed_bases": [BASE],
    "slm_prompt": (
        "<|start_of_role|>system<|end_of_role|>{system}<|end_of_text|>\n"
        "<|start_of_role|>user<|end_of_role|>{user}<|end_of_text|>\n"
        "<|start_of_role|>assistant<|end_of_role|>"
    ),
    "stop": ["<|end_of_text|>"],
    "max_tokens": 3072,
    "pass_threshold": 0.6,
}


def out_dir() -> Path:
    path = Path(os.environ.get("CCSLM_OUT") or ROOT / "out")
    path.mkdir(parents=True, exist_ok=True)
    return path


def log(message: str) -> None:
    stamp = time.strftime("%H:%M:%S")
    print(f"[{stamp}] {message}", file=sys.stderr, flush=True)


def read_jsonl(path: Path | str) -> list[dict[str, Any]]:
    with Path(path).open(encoding="utf-8") as handle:
        return [json.loads(line) for line in handle if line.strip()]


def iter_jsonl(path: Path | str) -> Iterator[dict[str, Any]]:
    with Path(path).open(encoding="utf-8") as handle:
        for line in handle:
            if line.strip():
                yield json.loads(line)


def write_jsonl(path: Path | str, rows: Iterable[dict[str, Any]]) -> int:
    target = Path(path)
    target.parent.mkdir(parents=True, exist_ok=True)
    count = 0
    temp = target.with_suffix(target.suffix + ".tmp")
    with temp.open("w", encoding="utf-8") as handle:
        for row in rows:
            handle.write(json.dumps(row, ensure_ascii=False) + "\n")
            count += 1
    temp.replace(target)
    return count


def write_json(path: Path | str, value: Any) -> None:
    target = Path(path)
    target.parent.mkdir(parents=True, exist_ok=True)
    target.write_text(json.dumps(value, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")


def read_json(path: Path | str, default: Any = None) -> Any:
    target = Path(path)
    if not target.is_file():
        return default
    return json.loads(target.read_text(encoding="utf-8"))


def definition(refresh: bool = False) -> dict[str, Any]:
    """The arena definition, cached under out/arena.json; refresh re-reads the live API."""
    cache = out_dir() / "arena.json"
    if cache.is_file() and not refresh:
        return json.loads(cache.read_text(encoding="utf-8"))["definition"]
    with urllib.request.urlopen(ARENA_URL, timeout=30) as response:  # noqa: S310
        document = json.loads(response.read().decode("utf-8"))
    write_json(cache, document)
    return document["definition"]


def check_definition(found: dict[str, Any]) -> list[str]:
    """Differences between the live definition and what this project was built for."""
    grading = found.get("grading") or {}
    generation = found.get("generation") or {}
    live = {
        "allowed_bases": found.get("allowed_bases"),
        "slm_prompt": found.get("slm_prompt"),
        "stop": generation.get("stop"),
        "max_tokens": generation.get("max_tokens"),
        "pass_threshold": grading.get("pass_threshold"),
    }
    problems = [f"{key}: built for {EXPECTED[key]!r}, live {value!r}" for key, value in live.items() if value != EXPECTED[key]]
    headings = grading.get("headings")
    if headings and tuple(headings) != HEADINGS:
        problems.append(f"headings changed: {headings}")
    return problems


def render_prompt(template: str, task: dict[str, Any]) -> str:
    """The validator's render_prompt: replace {key} with each task field, in task order."""
    rendered = template
    for key, value in task.items():
        rendered = rendered.replace("{" + str(key) + "}", str(value))
    return rendered


def slm_prompt(task: dict[str, Any]) -> str:
    return render_prompt(EXPECTED["slm_prompt"], task)


@lru_cache(maxsize=4)
def tokenizer(name: str = BASE, revision: str = BASE_REVISION) -> Any:
    from tokenizers import Tokenizer

    local = Path(name)
    if (local / "tokenizer.json").is_file():
        return Tokenizer.from_file(str(local / "tokenizer.json"))
    return Tokenizer.from_pretrained(name, revision=revision)


def count_tokens(text: str) -> int:
    """Token count as the grader counts the output: Granite tokenizer, default special token handling."""
    return len(tokenizer().encode(text).ids)


def base_dir() -> Path:
    """The local snapshot of the pinned base model, downloaded on first use.

    CCSLM_BASE_DIR points somewhere else; the tests use it for a tiny model of the same architecture.
    """
    override = os.environ.get("CCSLM_BASE_DIR")
    if override:
        return Path(override)
    from huggingface_hub import snapshot_download

    return Path(snapshot_download(BASE, revision=BASE_REVISION))


MAMBA_TYPES = ("mamba", "linear_attention")
ATTENTION_TYPES = ("attention", "full_attention")


def portable_config(model_config: dict[str, Any], reference: dict[str, Any]) -> dict[str, Any]:
    """The base's own config.json with this model's layer list, in the base's naming.

    transformers 5 renames Granite's layer types (mamba -> linear_attention, attention ->
    full_attention), writes time_step_limit as {"__float__": "Infinity"} and swaps torch_dtype for
    dtype when it saves. The serving engine and the validators load the file with their own
    versions, so a shipped model keeps the base's config and changes only what pruning changed
    (num_hidden_layers, layer_types) and what quantisation adds (quantization_config).
    """
    out = dict(reference)
    types = model_config.get("layer_types") or reference["layer_types"]
    out["layer_types"] = [
        "mamba" if t in MAMBA_TYPES else "attention" if t in ATTENTION_TYPES else t for t in types
    ]
    out["num_hidden_layers"] = len(out["layer_types"])
    if model_config.get("quantization_config"):
        out["quantization_config"] = model_config["quantization_config"]
    return out


def write_portable_config(model_dir: Path, reference_dir: Path | None = None) -> None:
    reference = json.loads(((reference_dir or base_dir()) / "config.json").read_text(encoding="utf-8"))
    found = json.loads((model_dir / "config.json").read_text(encoding="utf-8"))
    write_json(model_dir / "config.json", portable_config(found, reference))


class Stages:
    """Done markers under out/stages so the pipeline can resume."""

    def __init__(self, root: Path | None = None) -> None:
        self.root = (root or out_dir()) / "stages"
        self.root.mkdir(parents=True, exist_ok=True)

    def done(self, name: str) -> bool:
        return (self.root / f"{name}.done").is_file()

    def mark(self, name: str, info: dict[str, Any] | None = None) -> None:
        write_json(self.root / f"{name}.done", {"finished": time.time(), **(info or {})})

    def clear(self, name: str) -> None:
        (self.root / f"{name}.done").unlink(missing_ok=True)