File size: 21,179 Bytes
45f1d40
 
8165eae
 
 
 
 
 
45f1d40
 
 
 
 
 
 
6a668e6
45f1d40
 
 
 
ef6b357
45f1d40
7995d55
45f1d40
b91f46c
45f1d40
bba8c7f
45f1d40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3d95bd
45f1d40
e3d95bd
 
45f1d40
e3d95bd
 
 
 
 
 
 
 
 
 
 
45f1d40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3d95bd
 
7995d55
e3d95bd
7995d55
 
45f1d40
 
 
 
7995d55
 
 
 
 
 
 
 
45f1d40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3d95bd
45f1d40
e3d95bd
 
45f1d40
e3d95bd
 
45f1d40
 
e3d95bd
 
 
 
 
 
 
 
 
 
 
45f1d40
 
e3d95bd
45f1d40
 
e3d95bd
 
 
 
 
 
 
 
 
 
 
 
 
45f1d40
e3d95bd
 
 
 
 
 
 
 
 
45f1d40
e3d95bd
 
 
 
45f1d40
8165eae
 
e3d95bd
 
 
 
8165eae
e3d95bd
 
 
8165eae
e3d95bd
 
8165eae
45f1d40
 
 
e3d95bd
 
 
45f1d40
 
 
 
 
 
e3d95bd
45f1d40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7995d55
 
 
 
 
 
 
 
 
 
 
45f1d40
1f22abe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7995d55
 
1f22abe
7995d55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1f22abe
7995d55
 
 
 
 
 
 
 
 
 
 
 
1f22abe
 
b856632
 
 
1f22abe
b856632
 
 
1f22abe
7995d55
 
 
 
 
 
 
1f22abe
 
7995d55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1f22abe
7995d55
 
 
 
 
 
 
 
 
 
1f22abe
 
 
 
45f1d40
1f22abe
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
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
# Phase 4 rehearsal on a CPU instance: every check a session can fail, run where failing costs no quota.
#
# The first GPU session of the run bills ~6.5 hours before it is provably healthy, and E-031/E-034 are both
# exactly the shape of bug that only shows up mid-run or at the very first launch. So this kernel does, on
# free CPU: fetches the pinned code and asserts its sha256s, resolves credentials, reads the checkpoint
# pointer, downloads the PUBLISHED mix from the Hub, hashes every shard against the manifest, rehearses the
# session planner across the whole schedule, and proves resume-equivalence of the reader at the boundaries
# the run actually crosses.
#
# It ends with the same PLAN_JSON the GPU session will print, so the numbers can be compared before and
# after rather than trusting that the code did not change in between.
import hashlib, json, os, subprocess, sys, time

os.chdir("/kaggle/working")
sys.path.insert(0, "/kaggle/working")
REV = os.environ.get("OUNCE100M_REV") or "c237b478076f68da43dabd3403daa5639286899e"
WANT = {
  "ounce100m_credentials.py": ("ounce100m_credentials.py",
                               "6525f62f03f2d73650a1eb4f70fcb52d1194caad4ca88b2d8bd8fd54f88339b6"),
  "shard_dataset.py": ("train/shard_dataset.py",
                       "f35653bf4c8f2cfe7bb0c2c7835e308505fb5a84b4eb002d0f82f2cada768ca6"),
  "hubckpt.py": ("train/hubckpt.py",
                 "d4b50ed0928c678c94ce6764612a4655f2959c0a7b91d6fd588e8efddff298b2"),
  "train_ounce100m.py": ("train/train_ounce100m.py",
                         "dcb0ac199c0616575423ddea0d6b76d2257c06089d3f73aae0e0359de958ff03"),
  "phase4_session.py": ("kernels/phase4_session.py",
                        "5fb14bc3597d999a21772a66b6f409f01f722019dc78da313feddb2f9c769c48"),
}
BASE = "https://huggingface.co/Cion-lab/ounce100m-code/resolve/" + REV
for p, (rp, want) in sorted(WANT.items()):
    r = subprocess.run(["curl", "-sfL", BASE + "/" + rp, "-o", p], capture_output=True, text=True)
    assert r.returncode == 0, ("fetch failed", rp, (r.stderr or "")[:200])
    got = hashlib.sha256(open(p, "rb").read()).hexdigest()
    assert got == want, ("SHA MISMATCH", rp, got, want)
    print("OK", p, got[:12], flush=True)
print("REV_OK", REV, flush=True)

import ounce100m_credentials as C
print("creds:", json.dumps(C.install(verify=True)), flush=True)
T0 = time.time()


def run(argv, label, timeout, env=None):
    print("=== " + label, flush=True)
    t0 = time.time()
    e = dict(os.environ); e["PYTHONPATH"] = "/kaggle/working"
    e.update(env or {})
    import signal as _sig
    import threading
    p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
                         env=e, bufsize=1, start_new_session=True)
    killed = []

    def _kill():
        # Kill the group, not the child: a staged `python -c snapshot_download` orphaned by a timeout
        # keeps writing into mixroot while the next stage reads it.
        killed.append(True)
        try:
            os.killpg(os.getpgid(p.pid), _sig.SIGTERM)
        except Exception:
            p.kill()

    timer = threading.Timer(timeout, _kill)
    timer.daemon = True
    timer.start()
    out = []
    try:
        for line in p.stdout:
            out.append(line.rstrip("\n"))
            print("   |", line[:300], flush=True)
    finally:
        timer.cancel()
    rc = p.wait()
    if killed:
        print("   TIMEOUT after %d s, killed" % timeout, flush=True)
        rc = -9
    print("%s_RC %s seconds %.1f elapsed %.0f" % (label, rc, time.time() - t0, time.time() - T0),
          flush=True)
    return rc, "\n".join(out)


# Stage 1 -- the launcher itself, in rehearsal mode. On a CPU box torchrun is never reached: PREP_ONLY
# stops right after the pointer read, the mix download and the planner. Everything upstream of the first
# optimiser step is exercised with the real files and the real Hub.
PLANENV = {"PLANNING_TOK_PER_S": os.environ.get("PLANNING_TOK_PER_S", "9358"),
           "SESSION_GPU_HOURS": os.environ.get("SESSION_GPU_HOURS", "6.9"),
           "QUOTA_LEFT_HOURS": os.environ.get("QUOTA_LEFT_HOURS", "26.6"),
           "MIN_FREE_GB": "6"}
rc1, out1 = run([sys.executable, "phase4_session.py", REV], "PREP_LAUNCHER", 3000,
                env=dict(PLANENV, PHASE4_PREP_ONLY="1"))
if rc1 != 0:
    print("VERDICT PREP_STOP launcher rehearsal rc", rc1, flush=True)
    raise SystemExit(2)

# The launcher prints the argument list it *would* hand to torchrun, so stage 3 can parse exactly those
# flags against the pinned trainer. Duplicating them here would test a copy rather than the real thing.
ARGV_LINE = next((l for l in (out1 or "").splitlines() if l.startswith("TRAIN_ARGV ")), None)
assert ARGV_LINE, ("PREP_STOP: the launcher printed no TRAIN_ARGV -- its torchrun command line is "
                   "untested, which is how E-037 reached a billed session")
json.dump(json.loads(ARGV_LINE[len("TRAIN_ARGV "):]), open("train_argv.json", "w"))
print("TRAIN_ARGV captured", len(json.loads(ARGV_LINE[len("TRAIN_ARGV "):])), "tokens", flush=True)

# Stage 2 -- the published mix, byte for byte, and the reader at the boundaries the run will actually
# cross. `snapshot_download` proves the repo is anonymously readable; this proves the bytes are the ones
# the manifest certifies and that a resume at step X reads the same data an uninterrupted run would have.
CHECK = """
import hashlib, json, os, sys
sys.path.insert(0, "/kaggle/working")
ROOT = "/kaggle/working/mixroot"
man = json.load(open(os.path.join(ROOT, "manifest.json")))
tok_s = int(man["total_tokens"])
bad, nb = [], 0
for grp in ("shards", "val_shards"):
    for rec in man.get(grp, []):
        p = os.path.join(ROOT, rec["file"])
        if not os.path.exists(p):
            bad.append((rec["file"], "absent")); continue
        h = hashlib.sha256()
        with open(p, "rb") as fh:
            for chunk in iter(lambda: fh.read(1 << 22), b""):
                h.update(chunk)
        nb += 1
        if h.hexdigest() != rec["sha256"]:
            bad.append((rec["file"], "sha " + h.hexdigest()[:12] + " != " + rec["sha256"][:12]))
print("SHASHED", nb, "files", "total_tokens", format(tok_s, ","), "mismatches", len(bad), flush=True)
for b in bad[:10]:
    print("  BAD", b, flush=True)
assert not bad, "the published mix does not match its own manifest"

import phase4_session as S          # constants and plan() only -- main() is behind __name__
import shard_dataset as SD
SEQ, steps = S.SEQ_LEN, S.HORIZON_STEPS
seqs_per_step = S.MICRO_BATCH * S.WORLD * S.ACCUM       # the launcher's own geometry, not retyped
tps = seqs_per_step * SEQ
assert tps == S.TOKENS_PER_STEP, f"prep disagrees with the launcher about tokens/step: {tps}"
assert steps == int(S.TOKENS / tps), f"horizon disagrees: {steps}"
store = SD.PackedTokenStore(ROOT)
n_samples = store.total_tokens // SEQ
fingerprint = SD.mix_fingerprint(ROOT, man)
# The manifest is the corpus definition, and three different things have to agree about it: the record
# list, the counts it claims, and the bytes the reader actually mapped. A rebuild that truncated the list
# while leaving total_tokens alone would otherwise certify 1.11 B tokens of a smaller corpus (review).
if len(man["shards"]) != int(man.get("n_shards") or -1) or len(store.shards) != len(man["shards"]):
    raise AssertionError(f"shard records {len(man['shards'])} vs n_shards {man.get('n_shards')} vs "
                         f"files opened {len(store.shards)}")
summed = sum(int(s["tokens"]) for s in man["shards"])
if summed != tok_s or store.total_tokens != summed:
    raise AssertionError(f"manifest does not add up: records sum {summed}, total_tokens says {tok_s}, "
                         f"bytes the reader mapped say {store.total_tokens}")
print("READER", format(store.total_tokens, ","), "tokens in", len(store.shards), "shards ->",
      format(n_samples, ","), "windows of", SEQ, "|", steps, "steps x", tps, "tok =",
      format(steps * tps, ","), "tokens | fingerprint", fingerprint, flush=True)
assert n_samples >= steps * seqs_per_step, "not enough windows for the horizon"

# The schedule the run will actually live through: drive the real planner from step 0 until it reports the
# horizon, and require every leg to start where the previous one stopped. This is the check that catches a
# planner which stops one interval short of the end -- the last four steps of the run, unreachable, while
# every later session fails its own budget test for want of a whole interval.
legs, start = [], 0
while start < steps and len(legs) < 40:
    pl = S.plan(start, 15.0)
    if not pl["planned_steps"]:
        raise AssertionError(f"the planner refuses to start at step {start} with "
                             f"{pl['usable_hours']} usable hours -- the run can never finish")
    end = steps if pl["stop_after_steps"] == 0 else pl["stop_after_steps"]
    assert end > start, (start, end)
    need = (steps - start) * seqs_per_step              # the trainer's own guard, at this start step
    avail = n_samples - start * seqs_per_step
    assert avail >= need, (start, need, avail)
    store.tokens((end * seqs_per_step - 1) * SEQ, SEQ)  # last window this leg reads must be addressable
    legs.append((start, end))
    print("LEG %5d -> %5d  pct %5.1f -> %5.1f  need %9d avail %9d" % (
        start, end, 100.0 * start / steps, 100.0 * end / steps, need, avail), flush=True)
    start = end
print("SESSIONS", len(legs), "ends_at", legs[-1][1], "horizon", steps, "contiguous",
      [l[0] for l in legs[1:]] == [l[1] for l in legs[:-1]], flush=True)
assert legs[-1][1] == steps and [l[0] for l in legs[1:]] == [l[1] for l in legs[:-1]], legs

# Resume-equivalence through the cursor the trainer would actually write, at boundaries the run crosses:
# the first checkpoint, a mid-session one, and the last interval before the horizon. Building the resumed
# dataset from load_cursor() rather than a hand-computed offset is the point -- the derivation from
# step to samples_consumed is part of what is being tested.
full = SD.make_dataset(store, SEQ, 20260919, start_sample=0)
order = SD.perm_sha(n_samples, 20260919)
assert full.order_sha == order, "two derivations of the order identity disagree"
for boundary in sorted({S.CKPT_EVERY, legs[len(legs) // 2][0], steps - S.CKPT_EVERY}):
    d = "/tmp/cur_%d" % boundary
    os.makedirs(d, exist_ok=True)
    SD.save_cursor(d, SD.cursor_dict(SEQ, boundary * seqs_per_step, 20260919, fingerprint,
                                     step=boundary, order_sha=order))
    c = SD.load_cursor(d)
    assert c["step"] * seqs_per_step == c["samples_consumed"], c
    assert c["dataset_files_sha"] == fingerprint, c
    assert c["shuffle_perm_sha"] == order, c
    s0 = c["samples_consumed"]
    resumed = SD.make_dataset(store, SEQ, c["shuffle_seed"], start_sample=s0)
    assert resumed.order_sha == order, "a resume lost the visit order"
    assert len(resumed) == len(full) - s0, (len(resumed), len(full), s0)
    same = True
    for j in (0, 1, len(resumed) // 2, len(resumed) - 1):
        a, b = full[s0 + j], resumed[j]
        same = same and a["input_ids"].tolist() == b["input_ids"].tolist() \
            and a["labels"].tolist() == b["labels"].tolist()
    print("RESUME_EQUIV step", boundary, "start_sample", s0, "len", len(resumed), "identical", same,
          flush=True)
    assert same, boundary
print("CHECK_OK windows", format(n_samples, ","), "shards", len(store.shards), flush=True)
"""
open("prepcheck.py", "w").write(CHECK)
rc2, _ = run([sys.executable, "prepcheck.py"], "CHECK_PUBLISHED_MIX", 2400, env=dict(PLANENV))

# Stage 3 -- the reader's cursor format round-trips, and the trainer imports and prints --help on this
# image. Both are cheap and both have bitten before (E-028: a build script that could not import its
# sibling module surfaced only after a GPU session had already been billed).
rc3, _ = run([sys.executable, "-c",
              "import sys; sys.path.insert(0, '/kaggle/working')\n"
              "import shard_dataset as SD, json, os\n"
              "os.makedirs('/tmp/cur', exist_ok=True)\n"
              "c = SD.cursor_dict(1024, 97536, 20260919, dataset_files_sha='ab' * 32, step=381)\n"
              "SD.save_cursor('/tmp/cur', c)\n"
              "back = SD.load_cursor('/tmp/cur')\n"
              "print('CURSOR', json.dumps(back), 'roundtrip', back == c)\n"
              "assert back == c\n"
              "import subprocess\n"
              "p = subprocess.run([sys.executable, 'train_ounce100m.py', '--help'],\n"
              "                   capture_output=True, text=True)\n"
              "print('TRAINER_HELP_RC', p.returncode, 'lines', len((p.stdout or '').splitlines()))\n"
              "assert p.returncode == 0, (p.stdout or '')[-1500:] + (p.stderr or '')[-1500:]\n"
              # The launcher's own flag list, parsed by the pinned trainer. argparse validates every
              # option it has already consumed by the time it reaches a trailing --help, so rc 0 here
              # means the real session command line is well-formed -- and rc 2 names the flag that is not.
              "argv = json.load(open('/kaggle/working/train_argv.json'))\n"
              "p2 = subprocess.run([sys.executable, 'train_ounce100m.py'] + argv[1:] + ['--help'],\n"
              "                    capture_output=True, text=True)\n"
              "out2 = (p2.stdout or '') + (p2.stderr or '')\n"
              "print('LAUNCHER_ARGV_RC', p2.returncode, 'flags',\n"
              "      sum(1 for a in argv if a.startswith('--')))\n"
              "assert p2.returncode == 0, out2[-1200:]\n"],
             "CURSOR_AND_IMPORT", 900)

# Stage 4 -- checkpoint verification against real Hub bytes, on the theory that a gate which authorises
# deleting the only local copy of a checkpoint should be tested where failing is free. Probe v5 measured
# the old whole-object read-back at `push+verify=797.7s` for an 850 MB optimizer.pt (and 26 s for the same
# cycle once the CDN was warm), so the read-back is now two bounded Range requests; this proves the
# replacement still says yes to the 1.27 GB the probe really pushed and still says no three different ways.
# PREP_VERIFY_REPO exists because the probe repos are scratch: point it at `Cion-lab/ounce100m-ckpt` plus a
# `ckpt/checkpoint-N` once the run has one and this stage outlives them.
VERIFY = """
import json, os, sys, time
sys.path.insert(0, "/kaggle/working")
import hubckpt
from huggingface_hub import snapshot_download
REPO = os.environ.get("PREP_VERIFY_REPO") or "Cion-lab/ounce100m-ckpt-probe-0920-0747"
SUB = os.environ.get("PREP_VERIFY_SUB") or "ckpt/checkpoint-10"
d = "/kaggle/working/verify_subject"
t0 = time.time()
snapshot_download(repo_id=REPO, repo_type="dataset", allow_patterns=SUB + "/*", local_dir=d,
                  max_workers=4)
local = os.path.join(d, SUB)
print("GOT", len(os.listdir(local)), "files from", REPO, SUB, "in", round(time.time() - t0, 1), "s",
      flush=True)
assert os.path.exists(os.path.join(local, "model.safetensors")), sorted(os.listdir(local))

t0 = time.time()
v = hubckpt.verify_checkpoint(REPO, local, SUB, repo_type="dataset")
secs = round(time.time() - t0, 1)
print("VERIFY_CLEAN ok", v["ok"], "readback_ok", v["readback_ok"], "seconds", secs,
      json.dumps({k: v[k] for k in ("n_local", "n_hub", "missing", "mismatch", "extra", "readback")},
                 default=str)[:600], flush=True)
assert v["ok"] and v["readback_ok"], v
assert secs < 180, f"verification took {secs}s -- the bounded read-back was supposed to make this cheap"
rb = v["readback"]
assert rb["path"] == "optimizer.pt" and rb["bytes"] < (10 << 20) and len(rb["ranges"]) == 2, rb

# 1. A byte the Hub does not have, flipped INSIDE the read-back window, so the byte compare itself is what
#    catches it (the oid compare would too, but only the compare proves the served offset is ours).
p = os.path.join(local, "optimizer.pt")
size = hubckpt.hub_listing(REPO, "dataset")[SUB + "/optimizer.pt"][0]
print("SUBJECT", size, "bytes; ranges", [[0, (4 << 20) - 1], [size // 2, size // 2 + (4 << 20) - 1]],
      flush=True)


def tamper(offset):
    with open(p, "r+b") as fh:
        fh.seek(offset)
        b = fh.read(1)
        fh.seek(offset)
        fh.write(bytes([b[0] ^ 0xFF]))
    return b, offset


def restore(b, offset):
    with open(p, "r+b") as fh:
        fh.seek(offset)
        fh.write(b)


b, off = tamper(12345)
v2 = hubckpt.verify_checkpoint(REPO, local, SUB, repo_type="dataset")
print("VERIFY_TAMPERED_IN_RANGE ok", v2["ok"], "readback",
      json.dumps(v2["readback"].get("error", ""))[:200], flush=True)
assert not v2["ok"], "a flipped byte at 12,345 verified clean"
assert "differ from the local file" in str(v2["readback"]["error"]), v2["readback"]
restore(b, off)

b, off = tamper(500_000_001)          # outside both windows: only the stored-oid compare can see this
v2b = hubckpt.verify_checkpoint(REPO, local, SUB, repo_type="dataset")
print("VERIFY_TAMPERED_OUTSIDE ok", v2b["ok"], "mismatch", json.dumps(v2b["mismatch"])[:200],
      "readback_ok", v2b["readback_ok"], flush=True)
assert not v2b["ok"] and v2b["mismatch"] and v2b["readback_ok"], "the oid compare did not see it"
restore(b, off)
assert hubckpt.verify_checkpoint(REPO, local, SUB, repo_type="dataset")["ok"], "restoring did not help"

# 2. A path that is not on the Hub at all: nothing listed means nothing verified. `missing` is capped at
# ten entries because it is a log field, so the uncapped fact (`n_hub`) is what the assertion uses --
# rehearsing this stage caught that, not a defect in the verifier.
v3 = hubckpt.verify_checkpoint(REPO, local, "ckpt/checkpoint-99999999", repo_type="dataset")
print("VERIFY_ABSENT ok", v3["ok"], "n_hub", v3["n_hub"], "missing(shown)", len(v3["missing"]),
      "of n_local", v3["n_local"], flush=True)
assert not v3["ok"] and v3["n_hub"] == 0 and v3["missing"], v3


# 3. Three dishonest servers, each caught by a different clause of the bounded read-back.
class _Resp:
    def __init__(self, status, body, crange):
        self.status, self._b, self._cr = status, body, crange
        self.headers = {} if crange is None else {"Content-Range": crange}

    def __enter__(self): return self
    def __exit__(self, *a): return False
    def read(self, n=-1): return self._b if n is None or n < 0 else self._b[:n]


def _fake(kind):
    def f(req, *a, **k):
        if "/resolve/" not in getattr(req, "full_url", str(req)):
            return real(req, *a, **k)     # hub_listing shares urlopen; only the download is faked
        rng = req.get_header("Range") or ""
        lo, _, hi = rng.split("=")[-1].partition("-")
        n = int(hi) - int(lo) + 1 if lo.isdigit() and hi.isdigit() else 0
        if kind == "ignores_range":       # 200 with a short body and no Content-Range at all
            return _Resp(200, b"x" * max(n - 1, 0), None)
        if kind == "wrong_span":          # 206, exactly the bytes asked for, from the wrong offset
            return _Resp(206, b"y" * n, f"bytes 0-{n - 1}/{n}")
        return _Resp(206, bytes(n), f"bytes {lo}-{hi}/{size}")   # right span, someone else's bytes
    return f


real = hubckpt.urllib.request.urlopen
for kind, needle in (("ignores_range", "expected 206"), ("wrong_span", "Content-Range"),
                     ("wrong_bytes", "differ from the local file")):
    hubckpt.urllib.request.urlopen = _fake(kind)
    try:
        vx = hubckpt.verify_checkpoint(REPO, local, SUB, repo_type="dataset")
    finally:
        hubckpt.urllib.request.urlopen = real
    print("VERIFY_" + kind.upper(), "ok", vx["ok"], json.dumps(vx["readback"].get("error"))[:190],
          flush=True)
    assert not vx["ok"] and needle in str(vx["readback"]["error"]), (kind, vx["readback"])

# 4. The deletion guard itself (E-042): an unverified result must leave the directory alone.
d2 = "/tmp/prune_verified_probe"
os.makedirs(d2, exist_ok=True)
open(os.path.join(d2, "model.safetensors"), "wb").write(b"z" * 1024)
try:
    hubckpt.prune_verified(d2, {"verify": {"ok": False, "missing": ["a"], "mismatch": [],
                                           "extra": [], "readback_ok": False}}, min_free_gb=0.0)
    raise AssertionError("prune_verified deleted an unverified checkpoint")
except RuntimeError as e:
    print("PRUNE_BLOCKED", str(e)[:150], "dir_still_there", os.path.isdir(d2), flush=True)
    assert os.path.isdir(d2), "the copy was deleted anyway"
hubckpt.prune_verified(d2, {"verify": {"ok": True, "missing": [], "mismatch": [], "extra": [],
                                       "readback_ok": True}}, min_free_gb=0.0)
assert not os.path.isdir(d2), "a verified checkpoint was not pruned"
print("VERIFY_STAGE_OK clean, two tampers, absent, three dishonest servers, prune guard", flush=True)
"""
open("prepverify.py", "w").write(VERIFY)
rc4, _ = run([sys.executable, "prepverify.py"], "CHECK_VERIFY", 1800, env=dict(PLANENV))
print("VERDICT PHASE4_PREP rc_launcher", rc1, "rc_check", rc2, "rc_cursor", rc3, "rc_verify", rc4,
      "seconds", round(time.time() - T0, 1), flush=True)
raise SystemExit(0 if (rc1 == rc2 == rc3 == rc4 == 0) else 4)