ttishere commited on
Commit
5c32ff3
Β·
verified Β·
1 Parent(s): 9ad45b8

Publish code/calib

Browse files
Files changed (2) hide show
  1. code/calib/.gitkeep +0 -0
  2. code/calib/build_calib.py +276 -0
code/calib/.gitkeep ADDED
File without changes
code/calib/build_calib.py ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ build_calib.py -- build the calibration set for one arm.
3
+
4
+ This is the ONLY module where BASE and OURS differ. The difference is a single
5
+ switch: whether calibration tokens are noised.
6
+
7
+ BASE (--arm base): t=0, no masking. Clean continuous 2048-token windows.
8
+ OURS (--arm ours): per window t~U[0,1]; each token masked w.p. t -> MASK_ID.
9
+ No prefix protection (continuous windows, no prompt/answer).
10
+
11
+ Window construction is IDENTICAL and deterministic across arms: the same seed
12
+ produces byte-identical pre-noise windows, so OURS only ever *overwrites* masked
13
+ positions. This is what makes the β‘’(c) hash/byte-identity check pass.
14
+
15
+ Corpus:
16
+ --corpus c4 : pure C4-en (main experiment).
17
+ --corpus c4_cot : C4:CoT = 1:1, CoT from GSM8K TRAIN split only (ablation;
18
+ not run until the main result is in).
19
+
20
+ Outputs (to results/calib/ by default):
21
+ <prefix>_calib.pt : dict{input_ids: LongTensor[N, seqlen], windows_pre: ...}
22
+ <prefix>_manifest.json : full provenance (see below).
23
+ """
24
+ import os
25
+ import sys
26
+ import argparse
27
+ import numpy as np
28
+ import torch
29
+
30
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
31
+ import common as C
32
+
33
+
34
+ def build_windows(tokenizer, traindata, nsamples, seqlen, seed):
35
+ """
36
+ Deterministic continuous windows. Uses ONLY the window RNG (seed); no noise.
37
+ Returns LongTensor[nsamples, seqlen] and per-window pre-noise hashes.
38
+ Identical output for base and ours given the same (seed, nsamples, seqlen).
39
+ """
40
+ rng = np.random.default_rng(seed) # window-selection stream (arm-independent)
41
+ windows = []
42
+ hashes = []
43
+ attempts = 0
44
+ n_docs = len(traindata)
45
+ while len(windows) < nsamples:
46
+ attempts += 1
47
+ if attempts > nsamples * 500:
48
+ sys.exit(
49
+ f"ERROR: only built {len(windows)}/{nsamples} windows after "
50
+ f"{attempts} attempts. C4 cache too small?"
51
+ )
52
+ idx = int(rng.integers(0, n_docs))
53
+ enc = tokenizer(traindata[idx]["text"], return_tensors="pt", truncation=False)[
54
+ "input_ids"
55
+ ]
56
+ if enc.shape[1] <= seqlen:
57
+ continue
58
+ start = int(rng.integers(0, enc.shape[1] - seqlen))
59
+ chunk = enc[0, start : start + seqlen].clone().long()
60
+ windows.append(chunk)
61
+ hashes.append(C.sha256_ids(chunk))
62
+ return torch.stack(windows, dim=0), hashes
63
+
64
+
65
+ def apply_noise(windows, seed):
66
+ """
67
+ OURS noising. Separate RNG stream (does not perturb window selection).
68
+ Per window t~U[0,1]; token masked w.p. t. Returns (noised, t_list, stats).
69
+ """
70
+ gen = torch.Generator().manual_seed(seed)
71
+ N, L = windows.shape
72
+ t = torch.rand(N, generator=gen).tolist() # per-window mask probability
73
+ noised = windows.clone()
74
+ measured = []
75
+ thirds = [] # (front, mid, back) mask fraction per window -> positional bias check
76
+ third = L // 3
77
+ for i in range(N):
78
+ r = float(t[i])
79
+ m = torch.rand(L, generator=gen) < r
80
+ noised[i][m] = C.MASK_ID
81
+ measured.append(float(m.float().mean()))
82
+ thirds.append(
83
+ [
84
+ float(m[:third].float().mean()),
85
+ float(m[third : 2 * third].float().mean()),
86
+ float(m[2 * third :].float().mean()),
87
+ ]
88
+ )
89
+ return noised, t, measured, thirds
90
+
91
+
92
+ def build_windows_streaming(tokenizer, stream, nsamples, seqlen, seed, max_docs=200000):
93
+ """
94
+ Deterministic continuous windows from a STREAMING dataset (e.g. multilingual
95
+ mC4, which is too large to index). Iterates the stream in its fixed order;
96
+ for each doc long enough, takes one seeded-random seqlen window. Same seed +
97
+ same stream order => byte-identical windows across arms (base/ours).
98
+ """
99
+ rng = np.random.default_rng(seed)
100
+ windows, hashes = [], []
101
+ seen = 0
102
+ for ex in stream:
103
+ seen += 1
104
+ if seen > max_docs:
105
+ sys.exit(f"ERROR: streamed {seen} docs, only {len(windows)}/{nsamples} "
106
+ "windows; stream exhausted or docs too short.")
107
+ enc = tokenizer(ex["text"], return_tensors="pt", truncation=False)["input_ids"]
108
+ if enc.shape[1] <= seqlen:
109
+ continue
110
+ start = int(rng.integers(0, enc.shape[1] - seqlen))
111
+ chunk = enc[0, start : start + seqlen].clone().long()
112
+ windows.append(chunk)
113
+ hashes.append(C.sha256_ids(chunk))
114
+ if len(windows) >= nsamples:
115
+ break
116
+ return torch.stack(windows, dim=0), hashes
117
+
118
+
119
+ def load_corpus(corpus, c4_split, cot_split):
120
+ """Return list-like of {'text': ...} plus a composition record."""
121
+ from datasets import load_dataset
122
+
123
+ comp = {}
124
+ if corpus == "multilingual":
125
+ # true "不限语种": ROUND-ROBIN interleave of many per-language C4 streams.
126
+ # NB: the 'multilingual' config streams languages sequentially (af, am, ...),
127
+ # so taking the first N windows would be one language only -> we interleave
128
+ # explicit per-language streams to get a genuine mix.
129
+ from datasets import interleave_datasets
130
+
131
+ langs = ["en", "zh", "ru", "de", "fr", "es", "ja", "ar", "hi",
132
+ "pt", "it", "ko", "nl", "tr", "vi", "id"]
133
+ streams = [load_dataset("allenai/c4", l, split="train", streaming=True)
134
+ .select_columns(["text"]) # align features (timestamp types differ)
135
+ for l in langs]
136
+ ds = interleave_datasets(streams) # round-robin across languages
137
+ comp = {"c4": {"dataset": "allenai/c4", "config": "multilingual_interleaved",
138
+ "langs": langs, "split": "train(streaming)", "count": "streamed"},
139
+ "cot": {"dataset": None, "split": None, "count": 0}}
140
+ return ds, comp, None
141
+ if corpus == "c4":
142
+ c4 = load_dataset("allenai/c4", "en", split=c4_split)
143
+ comp = {"c4": {"dataset": "allenai/c4", "config": "en", "split": c4_split,
144
+ "count": len(c4)},
145
+ "cot": {"dataset": None, "split": None, "count": 0}}
146
+ return c4, comp, None
147
+ elif corpus == "c4_cot":
148
+ c4 = load_dataset("allenai/c4", "en", split=c4_split)
149
+ # CoT strictly from GSM8K TRAIN (never test).
150
+ assert "train" in cot_split, "CoT must come from GSM8K TRAIN split only."
151
+ gsm = load_dataset("gsm8k", "main", split=cot_split)
152
+ comp = {
153
+ "c4": {"dataset": "allenai/c4", "config": "en", "split": c4_split,
154
+ "count": len(c4)},
155
+ "cot": {"dataset": "gsm8k", "config": "main", "split": cot_split,
156
+ "count": len(gsm)},
157
+ }
158
+ return c4, comp, gsm
159
+ else:
160
+ sys.exit(f"unknown corpus {corpus}")
161
+
162
+
163
+ def main():
164
+ ap = argparse.ArgumentParser()
165
+ ap.add_argument("--arm", choices=["base", "ours"], required=True)
166
+ ap.add_argument("--corpus", choices=["c4", "c4_cot", "multilingual"], default="c4")
167
+ ap.add_argument("--nsamples", type=int, default=C.NSAMPLES)
168
+ ap.add_argument("--seqlen", type=int, default=C.SEQLEN)
169
+ ap.add_argument("--seed", type=int, default=42)
170
+ ap.add_argument("--model_path", type=str, default=C.DEFAULT_MODEL_PATH)
171
+ ap.add_argument("--c4_split", type=str, default="train[:100000]")
172
+ ap.add_argument("--cot_split", type=str, default="train")
173
+ ap.add_argument("--out_dir", type=str, default=None)
174
+ ap.add_argument("--dry_run", action="store_true",
175
+ help="build a tiny set + rich stats, do NOT save the big tensor")
176
+ ap.add_argument("--dry_n", type=int, default=16)
177
+ args = ap.parse_args()
178
+
179
+ from transformers import AutoTokenizer
180
+
181
+ tokenizer = AutoTokenizer.from_pretrained(args.model_path, trust_remote_code=True)
182
+
183
+ nsamples = args.dry_n if args.dry_run else args.nsamples
184
+ root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
185
+ out_dir = args.out_dir or os.path.join(root, "results", "calib")
186
+ os.makedirs(out_dir, exist_ok=True)
187
+
188
+ ghash = C.git_hash()
189
+ tag = "dry" if args.dry_run else "full"
190
+ prefix = f"{args.arm}_{args.corpus}_n{nsamples}_s{args.seed}_{tag}_{ghash}"
191
+
192
+ print(f"[calib] arm={args.arm} corpus={args.corpus} nsamples={nsamples} "
193
+ f"seqlen={args.seqlen} seed={args.seed}")
194
+
195
+ traindata, comp, cot = load_corpus(args.corpus, args.c4_split, args.cot_split)
196
+
197
+ # ── window construction (identical across arms) ───────────────────────────
198
+ if args.corpus == "multilingual":
199
+ windows, hashes = build_windows_streaming(tokenizer, traindata, nsamples,
200
+ args.seqlen, args.seed)
201
+ cot_used = 0
202
+ elif args.corpus == "c4":
203
+ windows, hashes = build_windows(tokenizer, traindata, nsamples, args.seqlen,
204
+ args.seed)
205
+ cot_used = 0
206
+ else:
207
+ # 1:1 interleave. Build C4 windows and CoT windows separately, then splice.
208
+ n_c4 = (nsamples + 1) // 2
209
+ n_cot = nsamples - n_c4
210
+ w_c4, h_c4 = build_windows(tokenizer, traindata, n_c4, args.seqlen, args.seed)
211
+ # CoT windows: concatenate q+a, pad/truncate to seqlen (separate builder omitted
212
+ # for main run; ablation wiring lives here but is not exercised yet).
213
+ cot_texts = [f"Question: {ex['question']}\nAnswer: {ex['answer']}" for ex in cot]
214
+ w_cot, h_cot = build_windows(
215
+ tokenizer, [{"text": t} for t in cot_texts], n_cot, args.seqlen,
216
+ args.seed + 1,
217
+ )
218
+ windows = torch.cat([w_c4, w_cot], dim=0)
219
+ hashes = h_c4 + h_cot
220
+ cot_used = n_cot
221
+
222
+ # ── the ONLY arm difference: noise ────────────────────────────────────────
223
+ if args.arm == "base":
224
+ input_ids = windows.clone()
225
+ t_list = [0.0] * nsamples
226
+ measured = [0.0] * nsamples
227
+ thirds = [[0.0, 0.0, 0.0]] * nsamples
228
+ n_mask_total = int((input_ids == C.MASK_ID).sum())
229
+ else:
230
+ input_ids, t_list, measured, thirds = apply_noise(windows, args.seed)
231
+ n_mask_total = int((input_ids == C.MASK_ID).sum())
232
+
233
+ # ── manifest ──────────────────────────────────────────────────────────────
234
+ manifest = {
235
+ "arm": args.arm,
236
+ "model_id": C.DEFAULT_MODEL_ID,
237
+ "noise_mode": "clean_t0" if args.arm == "base" else "uniform_t_U01",
238
+ "corpus": args.corpus,
239
+ "corpus_composition": comp,
240
+ "cot_windows_used": cot_used,
241
+ "nsamples": nsamples,
242
+ "seqlen": args.seqlen,
243
+ "seed": args.seed,
244
+ "mask_id": C.MASK_ID,
245
+ "git_hash": ghash,
246
+ "model_path": args.model_path,
247
+ "window_hashes_prenoise": hashes,
248
+ "per_sample_t": t_list,
249
+ "per_sample_measured_mask_ratio": measured,
250
+ "per_sample_mask_thirds": thirds,
251
+ "total_mask_tokens": n_mask_total,
252
+ "total_tokens": int(input_ids.numel()),
253
+ "dry_run": args.dry_run,
254
+ }
255
+ man_path = os.path.join(out_dir, f"{prefix}_manifest.json")
256
+ C.dump_json(manifest, man_path)
257
+ print(f"[calib] manifest -> {man_path}")
258
+
259
+ if not args.dry_run:
260
+ calib_path = os.path.join(out_dir, f"{prefix}_calib.pt")
261
+ torch.save({"input_ids": input_ids, "windows_pre": windows,
262
+ "window_hashes": hashes, "git_hash": ghash,
263
+ "arm": args.arm, "seed": args.seed}, calib_path)
264
+ print(f"[calib] tensor -> {calib_path} shape={tuple(input_ids.shape)}")
265
+ else:
266
+ # dry summary for β‘’ checks
267
+ import numpy as _np
268
+ meas = _np.array(measured)
269
+ tt = _np.array(t_list)
270
+ print(f"[dry] mask ratio mean={meas.mean():.4f} "
271
+ f"t mean={tt.mean():.4f} total_mask={n_mask_total}")
272
+ print(f"[dry] window hashes (first 3): {hashes[:3]}")
273
+
274
+
275
+ if __name__ == "__main__":
276
+ main()