quantgap 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantgap/__init__.py +3 -0
- quantgap/backends/__init__.py +60 -0
- quantgap/backends/base.py +118 -0
- quantgap/backends/hf.py +77 -0
- quantgap/backends/llamacpp.py +99 -0
- quantgap/backends/mlx.py +86 -0
- quantgap/backends/openai_compat.py +164 -0
- quantgap/cli.py +521 -0
- quantgap/config.py +60 -0
- quantgap/hub/__init__.py +1 -0
- quantgap/hub/packs.py +142 -0
- quantgap/hub/resolve.py +247 -0
- quantgap/hub/submit.py +87 -0
- quantgap/models.py +202 -0
- quantgap/pack/__init__.py +4 -0
- quantgap/pack/reader.py +142 -0
- quantgap/pack/runner.py +301 -0
- quantgap/pack/writer.py +162 -0
- quantgap/probes/__init__.py +11 -0
- quantgap/probes/build.py +484 -0
- quantgap/probes/data/SOURCES.md +15 -0
- quantgap/probes/data/probes_v1.jsonl +160 -0
- quantgap/probes/registry.py +34 -0
- quantgap/probes/render.py +158 -0
- quantgap/report/__init__.py +0 -0
- quantgap/report/chart.py +92 -0
- quantgap/report/jsonout.py +25 -0
- quantgap/report/table.py +82 -0
- quantgap/score/__init__.py +4 -0
- quantgap/score/compare.py +33 -0
- quantgap/score/damage.py +103 -0
- quantgap/score/kld.py +73 -0
- quantgap/score/recommend.py +78 -0
- quantgap/score/run.py +260 -0
- quantgap/score/stats.py +33 -0
- quantgap/tokcheck.py +71 -0
- quantgap/util.py +95 -0
- quantgap-0.1.0.dist-info/METADATA +311 -0
- quantgap-0.1.0.dist-info/RECORD +42 -0
- quantgap-0.1.0.dist-info/WHEEL +4 -0
- quantgap-0.1.0.dist-info/entry_points.txt +2 -0
- quantgap-0.1.0.dist-info/licenses/LICENSE +202 -0
quantgap/pack/reader.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Read a pack. Every file is checked against the SHA-256 in the manifest before it is used."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pyarrow.parquet as pq
|
|
11
|
+
|
|
12
|
+
from quantgap.models import Manifest
|
|
13
|
+
from quantgap.util import sha256_file
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PackIntegrityError(RuntimeError):
|
|
17
|
+
"""A pack file does not match the hash in its manifest (corrupt or edited)."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class SeqTopK:
|
|
22
|
+
"""The reference model's top-k guesses at the scored positions of one sequence."""
|
|
23
|
+
|
|
24
|
+
seq_id: int
|
|
25
|
+
pos: np.ndarray # (n,) position index; predicts token pos + 1
|
|
26
|
+
target: np.ndarray # (n,) the actual next token
|
|
27
|
+
ids: np.ndarray # (n, k) reference top-k ids, highest first
|
|
28
|
+
logp: np.ndarray # (n, k) their natural-log probabilities
|
|
29
|
+
logp_target: np.ndarray # (n,) reference log probability of the actual next token
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class ProbeRow:
|
|
34
|
+
seq_id: int
|
|
35
|
+
group: str
|
|
36
|
+
token_ids: np.ndarray
|
|
37
|
+
score_mask: np.ndarray
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class GreedyRow:
|
|
42
|
+
seq_id: int
|
|
43
|
+
group: str
|
|
44
|
+
lang: str
|
|
45
|
+
prompt_ids: list[int]
|
|
46
|
+
ref_ids: list[int]
|
|
47
|
+
ref_text: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Pack:
|
|
51
|
+
def __init__(self, root: Path, manifest: Manifest):
|
|
52
|
+
self.root = root
|
|
53
|
+
self.manifest = manifest
|
|
54
|
+
self._topk: dict[int, SeqTopK] | None = None
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def pack_id(self) -> str:
|
|
58
|
+
return self.manifest.pack_id
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def load(cls, path: str | Path, verify: bool = True) -> Pack:
|
|
62
|
+
root = Path(path)
|
|
63
|
+
mf = root / "manifest.json"
|
|
64
|
+
if not mf.is_file():
|
|
65
|
+
raise FileNotFoundError(f"No manifest.json in {root}. Is this a quantgap pack folder?")
|
|
66
|
+
manifest = Manifest.model_validate(json.loads(mf.read_text(encoding="utf-8")))
|
|
67
|
+
pack = cls(root, manifest)
|
|
68
|
+
if verify:
|
|
69
|
+
pack.verify()
|
|
70
|
+
return pack
|
|
71
|
+
|
|
72
|
+
def verify(self) -> None:
|
|
73
|
+
for name, want in self.manifest.files.items():
|
|
74
|
+
p = self.root / name
|
|
75
|
+
if not p.is_file():
|
|
76
|
+
raise PackIntegrityError(f"Pack file missing: {name}")
|
|
77
|
+
got = sha256_file(p)
|
|
78
|
+
if got != want:
|
|
79
|
+
raise PackIntegrityError(
|
|
80
|
+
f"Hash mismatch for {name}: manifest says {want[:12]}..., file is {got[:12]}.... "
|
|
81
|
+
"The file was changed or the download is broken. Download the pack again."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def probes(self) -> list[ProbeRow]:
|
|
85
|
+
rows = pq.read_table(self.root / "probes.parquet").to_pylist()
|
|
86
|
+
return [
|
|
87
|
+
ProbeRow(
|
|
88
|
+
r["seq_id"],
|
|
89
|
+
r["group"],
|
|
90
|
+
np.array(r["token_ids"], np.int64),
|
|
91
|
+
np.array(r["score_mask"], bool),
|
|
92
|
+
)
|
|
93
|
+
for r in rows
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
def greedy(self) -> list[GreedyRow]:
|
|
97
|
+
rows = pq.read_table(self.root / "greedy.parquet").to_pylist()
|
|
98
|
+
return [
|
|
99
|
+
GreedyRow(
|
|
100
|
+
r["seq_id"], r["group"], r["lang"], r["prompt_ids"], r["ref_ids"], r["ref_text"]
|
|
101
|
+
)
|
|
102
|
+
for r in rows
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
def topk(self) -> dict[int, SeqTopK]:
|
|
106
|
+
if self._topk is not None:
|
|
107
|
+
return self._topk
|
|
108
|
+
k = self.manifest.k
|
|
109
|
+
parts: dict[int, list[dict[str, np.ndarray]]] = {}
|
|
110
|
+
for name in sorted(n for n in self.manifest.files if n.startswith("topk-")):
|
|
111
|
+
tb = pq.read_table(self.root / name)
|
|
112
|
+
seq = tb["seq_id"].to_numpy()
|
|
113
|
+
pos = tb["pos"].to_numpy()
|
|
114
|
+
target = tb["target"].to_numpy()
|
|
115
|
+
ids = tb["ids"].combine_chunks().values.to_numpy(zero_copy_only=False).reshape(-1, k)
|
|
116
|
+
logp = tb["logp"].combine_chunks().values.to_numpy(zero_copy_only=False).reshape(-1, k)
|
|
117
|
+
lpt = tb["logp_target"].to_numpy()
|
|
118
|
+
for s in np.unique(seq):
|
|
119
|
+
m = seq == s
|
|
120
|
+
parts.setdefault(int(s), []).append(
|
|
121
|
+
{
|
|
122
|
+
"pos": pos[m],
|
|
123
|
+
"target": target[m],
|
|
124
|
+
"ids": ids[m],
|
|
125
|
+
"logp": logp[m],
|
|
126
|
+
"lpt": lpt[m],
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
out: dict[int, SeqTopK] = {}
|
|
130
|
+
for s, chunks in parts.items():
|
|
131
|
+
cat = {key: np.concatenate([c[key] for c in chunks]) for key in chunks[0]}
|
|
132
|
+
order = np.argsort(cat["pos"], kind="stable")
|
|
133
|
+
out[s] = SeqTopK(
|
|
134
|
+
s,
|
|
135
|
+
cat["pos"][order],
|
|
136
|
+
cat["target"][order],
|
|
137
|
+
cat["ids"][order],
|
|
138
|
+
cat["logp"][order].astype(np.float64),
|
|
139
|
+
cat["lpt"][order].astype(np.float64),
|
|
140
|
+
)
|
|
141
|
+
self._topk = out
|
|
142
|
+
return out
|
quantgap/pack/runner.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""Run the reference model once and save its top-k next-token guesses (needs `quantgap[pack]`)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from quantgap import __version__
|
|
13
|
+
from quantgap.models import GreedyInfo, Manifest, ModelRef, RunnerInfo, TokenizerInfo
|
|
14
|
+
from quantgap.pack.reader import SeqTopK
|
|
15
|
+
from quantgap.pack.writer import PackWriter
|
|
16
|
+
from quantgap.probes.registry import load_probes, probes_sha256
|
|
17
|
+
from quantgap.probes.render import RenderedGreedy, RenderedSeq, render_probes
|
|
18
|
+
from quantgap.tokcheck import CHECK_STRING
|
|
19
|
+
|
|
20
|
+
POS_CHUNK = 128 # positions per log-softmax call, keeps memory flat for 150k-token vocabularies
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def tokenizer_info(tok: Any) -> TokenizerInfo:
|
|
24
|
+
try:
|
|
25
|
+
blob = tok.backend_tokenizer.to_str().encode("utf-8")
|
|
26
|
+
digest: str | None = hashlib.sha256(blob).hexdigest()
|
|
27
|
+
except Exception:
|
|
28
|
+
digest = None
|
|
29
|
+
vocab = len(tok)
|
|
30
|
+
bos_added = bool(tok("a", add_special_tokens=True)["input_ids"][:1] == [tok.bos_token_id])
|
|
31
|
+
return TokenizerInfo(
|
|
32
|
+
sha256=digest,
|
|
33
|
+
vocab_size=int(vocab),
|
|
34
|
+
bos=bos_added and tok.bos_token_id is not None,
|
|
35
|
+
bos_id=tok.bos_token_id,
|
|
36
|
+
eos_id=tok.eos_token_id,
|
|
37
|
+
check_ids=list(tok(CHECK_STRING, add_special_tokens=False)["input_ids"]),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def chat_template_hash(tok: Any) -> str | None:
|
|
42
|
+
tpl = getattr(tok, "chat_template", None)
|
|
43
|
+
if isinstance(tpl, str):
|
|
44
|
+
return hashlib.sha256(tpl.encode("utf-8")).hexdigest()
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _topk_for_sequence(model: Any, seq: RenderedSeq, k: int, device: Any) -> SeqTopK:
|
|
49
|
+
"""Forward pass for one sequence. Used for the unbatched path and for tests."""
|
|
50
|
+
return _topk_batch(model, [seq], k, device)[0]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _topk_batch(model: Any, seqs: list[RenderedSeq], k: int, device: Any) -> list[SeqTopK]:
|
|
54
|
+
import torch
|
|
55
|
+
|
|
56
|
+
max_len = max(len(s.token_ids) for s in seqs)
|
|
57
|
+
ids = torch.zeros((len(seqs), max_len), dtype=torch.long)
|
|
58
|
+
att = torch.zeros((len(seqs), max_len), dtype=torch.long)
|
|
59
|
+
for i, s in enumerate(seqs):
|
|
60
|
+
ids[i, : len(s.token_ids)] = torch.tensor(s.token_ids)
|
|
61
|
+
att[i, : len(s.token_ids)] = 1
|
|
62
|
+
with torch.no_grad():
|
|
63
|
+
logits = model(input_ids=ids.to(device), attention_mask=att.to(device)).logits
|
|
64
|
+
out: list[SeqTopK] = []
|
|
65
|
+
for i, s in enumerate(seqs):
|
|
66
|
+
mask = np.array(s.score_mask[1:], bool) # position p scored if token p+1 is scored
|
|
67
|
+
pos = np.nonzero(mask)[0]
|
|
68
|
+
target = np.array(s.token_ids, np.int64)[pos + 1]
|
|
69
|
+
tops_i, tops_v, tgt_lp = [], [], []
|
|
70
|
+
for a in range(0, len(pos), POS_CHUNK):
|
|
71
|
+
sel = torch.from_numpy(pos[a : a + POS_CHUNK]).to(device)
|
|
72
|
+
lp = torch.log_softmax(logits[i, sel].float(), dim=-1)
|
|
73
|
+
v, ix = torch.topk(lp, k, dim=-1)
|
|
74
|
+
t = torch.from_numpy(target[a : a + POS_CHUNK]).to(device)
|
|
75
|
+
tgt_lp.append(lp.gather(1, t[:, None])[:, 0].cpu().numpy())
|
|
76
|
+
tops_i.append(ix.cpu().numpy())
|
|
77
|
+
tops_v.append(v.cpu().numpy())
|
|
78
|
+
if len(pos) == 0:
|
|
79
|
+
out.append(
|
|
80
|
+
SeqTopK(
|
|
81
|
+
s.seq_id, pos, target, np.zeros((0, k), np.int64), np.zeros((0, k)), np.zeros(0)
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
continue
|
|
85
|
+
out.append(
|
|
86
|
+
SeqTopK(
|
|
87
|
+
s.seq_id,
|
|
88
|
+
pos.astype(np.int32),
|
|
89
|
+
target,
|
|
90
|
+
np.concatenate(tops_i),
|
|
91
|
+
np.concatenate(tops_v).astype(np.float64),
|
|
92
|
+
np.concatenate(tgt_lp).astype(np.float64),
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
return out
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def eos_ids(model: Any, tok: Any) -> set[int]:
|
|
99
|
+
"""Every id that ends a generation: the tokenizer's EOS plus the model's generation_config."""
|
|
100
|
+
out: set[int] = set()
|
|
101
|
+
if tok.eos_token_id is not None:
|
|
102
|
+
out.add(int(tok.eos_token_id))
|
|
103
|
+
gc = getattr(getattr(model, "generation_config", None), "eos_token_id", None)
|
|
104
|
+
if isinstance(gc, int):
|
|
105
|
+
out.add(gc)
|
|
106
|
+
elif gc:
|
|
107
|
+
out.update(int(x) for x in gc)
|
|
108
|
+
return out
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def greedy_generate(
|
|
112
|
+
model: Any, tok: Any, prompt_ids: list[int], max_new: int, device: Any
|
|
113
|
+
) -> list[int]:
|
|
114
|
+
"""Plain greedy decoding. Sampling settings from the model's generation_config (temperature,
|
|
115
|
+
top_p, top_k and above all repetition_penalty, which Qwen sets to 1.05 to 1.1) are switched
|
|
116
|
+
off on purpose: every backend must decode the same way or the comparison is unfair."""
|
|
117
|
+
import torch
|
|
118
|
+
|
|
119
|
+
inp = torch.tensor([prompt_ids], device=device)
|
|
120
|
+
with torch.no_grad():
|
|
121
|
+
out = model.generate(
|
|
122
|
+
input_ids=inp,
|
|
123
|
+
attention_mask=torch.ones_like(inp),
|
|
124
|
+
max_new_tokens=max_new,
|
|
125
|
+
do_sample=False,
|
|
126
|
+
temperature=None,
|
|
127
|
+
top_p=None,
|
|
128
|
+
top_k=None,
|
|
129
|
+
repetition_penalty=1.0,
|
|
130
|
+
pad_token_id=tok.pad_token_id if tok.pad_token_id is not None else tok.eos_token_id,
|
|
131
|
+
)
|
|
132
|
+
new = out[0, len(prompt_ids) :].tolist()
|
|
133
|
+
stop = eos_ids(model, tok)
|
|
134
|
+
for i, t in enumerate(new):
|
|
135
|
+
if t in stop:
|
|
136
|
+
return new[:i]
|
|
137
|
+
return new
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _greedy(model: Any, tok: Any, g: RenderedGreedy, max_new: int, device: Any) -> list[int]:
|
|
141
|
+
return greedy_generate(model, tok, g.prompt_ids, max_new, device)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def build_pack_from_objects(
|
|
145
|
+
model: Any,
|
|
146
|
+
tok: Any,
|
|
147
|
+
out_dir: str | Path,
|
|
148
|
+
*,
|
|
149
|
+
repo: str,
|
|
150
|
+
revision: str | None = None,
|
|
151
|
+
dtype: str = "bfloat16",
|
|
152
|
+
probes_version: str = "v1",
|
|
153
|
+
probes_file: Path | None = None,
|
|
154
|
+
k: int = 32,
|
|
155
|
+
batch_size: int = 4,
|
|
156
|
+
max_new_tokens: int = 256,
|
|
157
|
+
logp_dtype: str = "float32",
|
|
158
|
+
license_: str | None = None,
|
|
159
|
+
device: Any = "cpu",
|
|
160
|
+
device_label: str = "cpu",
|
|
161
|
+
progress: Callable[[int, int], None] | None = None,
|
|
162
|
+
max_len: int = 512,
|
|
163
|
+
) -> Manifest:
|
|
164
|
+
"""Build a pack from an already loaded model and tokenizer."""
|
|
165
|
+
import torch
|
|
166
|
+
import transformers
|
|
167
|
+
|
|
168
|
+
probes = load_probes(probes_version, probes_file)
|
|
169
|
+
seqs, greedy = render_probes(probes, tok, max_len=max_len)
|
|
170
|
+
writer = PackWriter(out_dir, k=k, logp_dtype=logp_dtype)
|
|
171
|
+
writer.write_probes(seqs)
|
|
172
|
+
model.eval()
|
|
173
|
+
|
|
174
|
+
order = sorted(range(len(seqs)), key=lambda i: len(seqs[i].token_ids))
|
|
175
|
+
done = 0
|
|
176
|
+
for a in range(0, len(order), batch_size):
|
|
177
|
+
batch = [seqs[i] for i in order[a : a + batch_size]]
|
|
178
|
+
for r in _topk_batch(model, batch, k, device):
|
|
179
|
+
writer.add_seq(r)
|
|
180
|
+
done += len(batch)
|
|
181
|
+
if progress:
|
|
182
|
+
progress(done, len(seqs) + len(greedy))
|
|
183
|
+
|
|
184
|
+
rows = []
|
|
185
|
+
for g in greedy:
|
|
186
|
+
ref = _greedy(model, tok, g, max_new_tokens, device)
|
|
187
|
+
rows.append(
|
|
188
|
+
{
|
|
189
|
+
"seq_id": g.seq_id,
|
|
190
|
+
"group": g.group,
|
|
191
|
+
"lang": g.lang,
|
|
192
|
+
"prompt_ids": g.prompt_ids,
|
|
193
|
+
"ref_ids": ref,
|
|
194
|
+
"ref_text": tok.decode(ref, skip_special_tokens=True),
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
done += 1
|
|
198
|
+
if progress:
|
|
199
|
+
progress(done, len(seqs) + len(greedy))
|
|
200
|
+
writer.write_greedy(rows)
|
|
201
|
+
|
|
202
|
+
return writer.finalize(
|
|
203
|
+
model=ModelRef(repo=repo, revision=revision, dtype=dtype, license=license_),
|
|
204
|
+
tokenizer=tokenizer_info(tok),
|
|
205
|
+
probes_set=probes_version,
|
|
206
|
+
probes_sha256=_probes_hash(probes_version, probes_file),
|
|
207
|
+
n_sequences=len(seqs),
|
|
208
|
+
chat_template_sha256=chat_template_hash(tok),
|
|
209
|
+
greedy=GreedyInfo(n_prompts=len(greedy), max_new_tokens=max_new_tokens),
|
|
210
|
+
runner=RunnerInfo(
|
|
211
|
+
quantgap=__version__,
|
|
212
|
+
torch=torch.__version__,
|
|
213
|
+
transformers=transformers.__version__,
|
|
214
|
+
device=device_label,
|
|
215
|
+
),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _probes_hash(version: str, path: Path | None) -> str:
|
|
220
|
+
if path is None:
|
|
221
|
+
return probes_sha256(version)
|
|
222
|
+
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def load_reference(
|
|
226
|
+
repo: str, revision: str | None, device: str = "auto"
|
|
227
|
+
) -> tuple[Any, Any, Any, str, str]:
|
|
228
|
+
"""Load the reference model. Returns (model, tokenizer, device, dtype_name, device_label)."""
|
|
229
|
+
import torch
|
|
230
|
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
231
|
+
|
|
232
|
+
has_cuda = torch.cuda.is_available()
|
|
233
|
+
use_cuda = device == "cuda" or (device == "auto" and has_cuda)
|
|
234
|
+
if use_cuda and not has_cuda:
|
|
235
|
+
raise RuntimeError("--device cuda was asked for, but no CUDA GPU was found.")
|
|
236
|
+
dtype = torch.bfloat16 if use_cuda else torch.float32
|
|
237
|
+
tok = AutoTokenizer.from_pretrained(repo, revision=revision)
|
|
238
|
+
# `torch_dtype` was renamed `dtype` in newer transformers; pass whichever this version knows.
|
|
239
|
+
import transformers
|
|
240
|
+
|
|
241
|
+
major_minor = tuple(int(x) for x in transformers.__version__.split(".")[:2])
|
|
242
|
+
dtype_key = "dtype" if major_minor >= (4, 56) else "torch_dtype"
|
|
243
|
+
kwargs: dict[str, Any] = {"revision": revision, dtype_key: dtype}
|
|
244
|
+
if use_cuda and torch.cuda.device_count() > 1:
|
|
245
|
+
kwargs["device_map"] = "auto" # spread across GPUs (Kaggle T4 x2), offload if needed
|
|
246
|
+
model = AutoModelForCausalLM.from_pretrained(repo, **kwargs)
|
|
247
|
+
if "device_map" in kwargs:
|
|
248
|
+
dev = next(model.parameters()).device
|
|
249
|
+
label = f"{torch.cuda.device_count()}x {torch.cuda.get_device_name(0)}"
|
|
250
|
+
elif use_cuda:
|
|
251
|
+
model = model.to("cuda")
|
|
252
|
+
dev = torch.device("cuda")
|
|
253
|
+
label = torch.cuda.get_device_name(0)
|
|
254
|
+
else:
|
|
255
|
+
dev = torch.device("cpu")
|
|
256
|
+
label = "cpu"
|
|
257
|
+
return model, tok, dev, str(dtype).replace("torch.", ""), label
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def refresh_greedy(
|
|
261
|
+
pack_dir: str | Path,
|
|
262
|
+
model: Any,
|
|
263
|
+
tok: Any,
|
|
264
|
+
device: Any = "cpu",
|
|
265
|
+
progress: Callable[[int, int], None] | None = None,
|
|
266
|
+
) -> Manifest:
|
|
267
|
+
"""Regenerate only greedy.parquet of an existing pack (keeps the top-k shards).
|
|
268
|
+
|
|
269
|
+
The pack id changes because the manifest holds the hash of every file."""
|
|
270
|
+
import json
|
|
271
|
+
|
|
272
|
+
from quantgap.pack.reader import Pack
|
|
273
|
+
from quantgap.util import sha256_file
|
|
274
|
+
|
|
275
|
+
pack_dir = Path(pack_dir)
|
|
276
|
+
pack = Pack.load(pack_dir)
|
|
277
|
+
old = pack.greedy()
|
|
278
|
+
rows = []
|
|
279
|
+
for i, g in enumerate(old):
|
|
280
|
+
ref = greedy_generate(
|
|
281
|
+
model, tok, list(g.prompt_ids), pack.manifest.greedy.max_new_tokens, device
|
|
282
|
+
)
|
|
283
|
+
rows.append(
|
|
284
|
+
{
|
|
285
|
+
"seq_id": g.seq_id,
|
|
286
|
+
"group": g.group,
|
|
287
|
+
"lang": g.lang,
|
|
288
|
+
"prompt_ids": list(g.prompt_ids),
|
|
289
|
+
"ref_ids": ref,
|
|
290
|
+
"ref_text": tok.decode(ref, skip_special_tokens=True),
|
|
291
|
+
}
|
|
292
|
+
)
|
|
293
|
+
if progress:
|
|
294
|
+
progress(i + 1, len(old))
|
|
295
|
+
PackWriter(pack_dir, k=pack.manifest.k, logp_dtype=pack.manifest.logp_dtype).write_greedy(rows)
|
|
296
|
+
files = dict(pack.manifest.files)
|
|
297
|
+
files["greedy.parquet"] = sha256_file(pack_dir / "greedy.parquet")
|
|
298
|
+
manifest = pack.manifest.model_copy(update={"files": files})
|
|
299
|
+
text = json.dumps(manifest.model_dump(by_alias=True, mode="json"), indent=2, ensure_ascii=False)
|
|
300
|
+
(pack_dir / "manifest.json").write_text(text + "\n", encoding="utf-8")
|
|
301
|
+
return manifest
|
quantgap/pack/writer.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Write a pack: parquet shards plus manifest.json, with a SHA-256 for every file."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pyarrow as pa
|
|
11
|
+
import pyarrow.parquet as pq
|
|
12
|
+
|
|
13
|
+
from quantgap.models import (
|
|
14
|
+
GreedyInfo,
|
|
15
|
+
Manifest,
|
|
16
|
+
ModelRef,
|
|
17
|
+
ProbesInfo,
|
|
18
|
+
RunnerInfo,
|
|
19
|
+
TokenizerInfo,
|
|
20
|
+
)
|
|
21
|
+
from quantgap.pack.reader import SeqTopK
|
|
22
|
+
from quantgap.probes.render import RenderedSeq
|
|
23
|
+
from quantgap.util import sha256_file, utc_now
|
|
24
|
+
|
|
25
|
+
SHARD_ROWS = 40_000
|
|
26
|
+
_COMPRESSION = "zstd"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _fixed_list(values: np.ndarray, width: int, dtype: pa.DataType) -> pa.Array:
|
|
30
|
+
n = len(values)
|
|
31
|
+
offsets = pa.array(np.arange(0, (n + 1) * width, width, dtype=np.int32))
|
|
32
|
+
return pa.ListArray.from_arrays(offsets, pa.array(values.reshape(-1), type=dtype))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _ragged_list(rows: list[np.ndarray], dtype: pa.DataType) -> pa.Array:
|
|
36
|
+
lengths = np.array([len(r) for r in rows], dtype=np.int64)
|
|
37
|
+
offsets = pa.array(np.concatenate([[0], np.cumsum(lengths)]).astype(np.int32))
|
|
38
|
+
flat = np.concatenate(rows) if rows else np.array([])
|
|
39
|
+
return pa.ListArray.from_arrays(offsets, pa.array(flat, type=dtype))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PackWriter:
|
|
43
|
+
def __init__(self, out_dir: str | Path, k: int, logp_dtype: str = "float32"):
|
|
44
|
+
self.out = Path(out_dir)
|
|
45
|
+
self.out.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
self.k = k
|
|
47
|
+
self.logp_dtype = logp_dtype
|
|
48
|
+
self._np_dtype = np.float16 if logp_dtype == "float16" else np.float32
|
|
49
|
+
self._pa_dtype = pa.float16() if logp_dtype == "float16" else pa.float32()
|
|
50
|
+
self._buf: list[SeqTopK] = []
|
|
51
|
+
self._buf_rows = 0
|
|
52
|
+
self._shards: list[str] = []
|
|
53
|
+
self.n_positions = 0
|
|
54
|
+
|
|
55
|
+
# ---- top-k shards
|
|
56
|
+
def add_seq(self, s: SeqTopK) -> None:
|
|
57
|
+
if s.ids.shape != (len(s.pos), self.k):
|
|
58
|
+
raise ValueError(f"ids must have shape (n_positions, {self.k}), got {s.ids.shape}")
|
|
59
|
+
self._buf.append(s)
|
|
60
|
+
self._buf_rows += len(s.pos)
|
|
61
|
+
self.n_positions += len(s.pos)
|
|
62
|
+
if self._buf_rows >= SHARD_ROWS:
|
|
63
|
+
self._flush()
|
|
64
|
+
|
|
65
|
+
def _flush(self) -> None:
|
|
66
|
+
if not self._buf:
|
|
67
|
+
return
|
|
68
|
+
seq_id = np.concatenate([np.full(len(s.pos), s.seq_id, np.int32) for s in self._buf])
|
|
69
|
+
pos = np.concatenate([s.pos for s in self._buf]).astype(np.int32)
|
|
70
|
+
target = np.concatenate([s.target for s in self._buf]).astype(np.int32)
|
|
71
|
+
ids = np.concatenate([s.ids for s in self._buf]).astype(np.int32)
|
|
72
|
+
logp = np.concatenate([s.logp for s in self._buf]).astype(self._np_dtype)
|
|
73
|
+
lpt = np.concatenate([s.logp_target for s in self._buf]).astype(np.float32)
|
|
74
|
+
table = pa.table(
|
|
75
|
+
{
|
|
76
|
+
"seq_id": pa.array(seq_id, pa.int32()),
|
|
77
|
+
"pos": pa.array(pos, pa.int32()),
|
|
78
|
+
"target": pa.array(target, pa.int32()),
|
|
79
|
+
"ids": _fixed_list(ids, self.k, pa.int32()),
|
|
80
|
+
"logp": _fixed_list(logp, self.k, self._pa_dtype),
|
|
81
|
+
"logp_target": pa.array(lpt, pa.float32()),
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
name = f"topk-{len(self._shards):05d}.parquet"
|
|
85
|
+
pq.write_table(table, self.out / name, compression=_COMPRESSION)
|
|
86
|
+
self._shards.append(name)
|
|
87
|
+
self._buf, self._buf_rows = [], 0
|
|
88
|
+
|
|
89
|
+
# ---- probes and greedy
|
|
90
|
+
def write_probes(self, seqs: Iterable[RenderedSeq]) -> None:
|
|
91
|
+
seqs = list(seqs)
|
|
92
|
+
table = pa.table(
|
|
93
|
+
{
|
|
94
|
+
"seq_id": pa.array([s.seq_id for s in seqs], pa.int32()),
|
|
95
|
+
"group": pa.array([s.group for s in seqs], pa.string()),
|
|
96
|
+
"token_ids": _ragged_list(
|
|
97
|
+
[np.array(s.token_ids, np.int32) for s in seqs], pa.int32()
|
|
98
|
+
),
|
|
99
|
+
"n_tokens": pa.array([len(s.token_ids) for s in seqs], pa.int32()),
|
|
100
|
+
"score_mask": _ragged_list(
|
|
101
|
+
[np.array(s.score_mask, bool) for s in seqs], pa.bool_()
|
|
102
|
+
),
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
pq.write_table(table, self.out / "probes.parquet", compression=_COMPRESSION)
|
|
106
|
+
|
|
107
|
+
def write_greedy(self, rows: list[dict]) -> None:
|
|
108
|
+
"""rows: dicts with seq_id, group, lang, prompt_ids, ref_ids, ref_text."""
|
|
109
|
+
table = pa.table(
|
|
110
|
+
{
|
|
111
|
+
"seq_id": pa.array([r["seq_id"] for r in rows], pa.int32()),
|
|
112
|
+
"group": pa.array([r["group"] for r in rows], pa.string()),
|
|
113
|
+
"lang": pa.array([r["lang"] for r in rows], pa.string()),
|
|
114
|
+
"prompt_ids": _ragged_list(
|
|
115
|
+
[np.array(r["prompt_ids"], np.int32) for r in rows], pa.int32()
|
|
116
|
+
),
|
|
117
|
+
"ref_ids": _ragged_list(
|
|
118
|
+
[np.array(r["ref_ids"], np.int32) for r in rows], pa.int32()
|
|
119
|
+
),
|
|
120
|
+
"ref_text": pa.array([r["ref_text"] for r in rows], pa.string()),
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
pq.write_table(table, self.out / "greedy.parquet", compression=_COMPRESSION)
|
|
124
|
+
|
|
125
|
+
# ---- manifest
|
|
126
|
+
def finalize(
|
|
127
|
+
self,
|
|
128
|
+
*,
|
|
129
|
+
model: ModelRef,
|
|
130
|
+
tokenizer: TokenizerInfo,
|
|
131
|
+
probes_set: str,
|
|
132
|
+
probes_sha256: str,
|
|
133
|
+
n_sequences: int,
|
|
134
|
+
chat_template_sha256: str | None,
|
|
135
|
+
greedy: GreedyInfo,
|
|
136
|
+
runner: RunnerInfo,
|
|
137
|
+
) -> Manifest:
|
|
138
|
+
self._flush()
|
|
139
|
+
files = {
|
|
140
|
+
name: sha256_file(self.out / name)
|
|
141
|
+
for name in sorted([*self._shards, "probes.parquet", "greedy.parquet"])
|
|
142
|
+
}
|
|
143
|
+
m = Manifest(
|
|
144
|
+
model=model,
|
|
145
|
+
tokenizer=tokenizer,
|
|
146
|
+
probes=ProbesInfo(
|
|
147
|
+
set=probes_set,
|
|
148
|
+
sha256=probes_sha256,
|
|
149
|
+
n_sequences=n_sequences,
|
|
150
|
+
n_positions=self.n_positions,
|
|
151
|
+
),
|
|
152
|
+
chat_template_sha256=chat_template_sha256,
|
|
153
|
+
k=self.k,
|
|
154
|
+
logp_dtype=self.logp_dtype,
|
|
155
|
+
greedy=greedy,
|
|
156
|
+
runner=runner,
|
|
157
|
+
files=files,
|
|
158
|
+
created_at=utc_now(),
|
|
159
|
+
)
|
|
160
|
+
text = json.dumps(m.model_dump(by_alias=True, mode="json"), indent=2, ensure_ascii=False)
|
|
161
|
+
(self.out / "manifest.json").write_text(text + "\n", encoding="utf-8")
|
|
162
|
+
return m
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from quantgap.probes.registry import load_probes, probes_path, probes_sha256
|
|
2
|
+
from quantgap.probes.render import RenderedGreedy, RenderedSeq, render_probes
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"RenderedGreedy",
|
|
6
|
+
"RenderedSeq",
|
|
7
|
+
"load_probes",
|
|
8
|
+
"probes_path",
|
|
9
|
+
"probes_sha256",
|
|
10
|
+
"render_probes",
|
|
11
|
+
]
|