perm_sha: identify the visit order by hashing the permutation, not just its seed (E-035 residual) -- cursor gains shuffle_perm_sha, asserted on resume; session repins the two files
Browse files- train/shard_dataset.py +32 -8
train/shard_dataset.py
CHANGED
|
@@ -80,6 +80,22 @@ class PackedTokenStore:
|
|
| 80 |
return out
|
| 81 |
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
def make_dataset(store, seq_len, seed, start_sample=0, count=None, shuffle=True):
|
| 84 |
"""A map-style torch Dataset of (input_ids, labels, attention_mask) with an exact resume cursor.
|
| 85 |
|
|
@@ -91,11 +107,14 @@ def make_dataset(store, seq_len, seed, start_sample=0, count=None, shuffle=True)
|
|
| 91 |
|
| 92 |
n_samples = store.total_tokens // seq_len
|
| 93 |
perm = None
|
|
|
|
| 94 |
if shuffle:
|
| 95 |
# Seed must be identical on every resume or the two runs read different data orders. Stored in
|
| 96 |
-
# the checkpoint (cursor.json) and asserted by the trainer
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
| 99 |
else:
|
| 100 |
perm = np.arange(start_sample, n_samples, dtype=np.int64)
|
| 101 |
if count is not None:
|
|
@@ -117,20 +136,25 @@ def make_dataset(store, seq_len, seed, start_sample=0, count=None, shuffle=True)
|
|
| 117 |
# these names keeps it from dropping anything.
|
| 118 |
column_names = ["input_ids", "labels", "attention_mask"]
|
| 119 |
|
| 120 |
-
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
|
| 124 |
-
def cursor_dict(seq_len, samples_consumed, seed, dataset_files_sha=None, step=None):
|
| 125 |
"""The whole of the data-position state. Written into every checkpoint, checked on every resume.
|
| 126 |
|
| 127 |
`step` is recorded alongside `samples_consumed` so the two can be cross-checked on resume: they are
|
| 128 |
redundant by construction (samples = step x sequences-per-step), and a cursor where they disagree is a
|
| 129 |
-
corrupted or hand-edited file, not a position to continue from.
|
|
|
|
| 130 |
"""
|
| 131 |
return {"seq_len": seq_len, "samples_consumed": samples_consumed,
|
| 132 |
"tokens_consumed": samples_consumed * seq_len, "shuffle_seed": seed,
|
| 133 |
-
"step": step, "dataset_files_sha": dataset_files_sha}
|
| 134 |
|
| 135 |
|
| 136 |
def load_cursor(dirpath):
|
|
|
|
| 80 |
return out
|
| 81 |
|
| 82 |
|
| 83 |
+
def perm_sha(n_samples, seed, base=None):
|
| 84 |
+
"""Identity of the visit order itself, not just the seed that produced it.
|
| 85 |
+
|
| 86 |
+
`seed` pins the order only for a fixed numpy. Kaggle updates its notebook image underneath us, and a
|
| 87 |
+
session that resumed after such a bump would read a different permutation from the same seed -- some
|
| 88 |
+
windows twice, others never -- while every other guard (weights, cursor arithmetic, fingerprint,
|
| 89 |
+
batch size) still agreed. Hashing the full permutation is ~10 ms and makes that state impossible to
|
| 90 |
+
enter quietly. Computed before the resume slice, so two sessions at different offsets agree on it.
|
| 91 |
+
`base` lets a caller that already generated the permutation avoid generating it twice.
|
| 92 |
+
"""
|
| 93 |
+
import hashlib
|
| 94 |
+
if base is None:
|
| 95 |
+
base = np.random.default_rng(np.random.PCG64(seed)).permutation(n_samples)
|
| 96 |
+
return hashlib.sha256(np.ascontiguousarray(base).tobytes()).hexdigest()[:16]
|
| 97 |
+
|
| 98 |
+
|
| 99 |
def make_dataset(store, seq_len, seed, start_sample=0, count=None, shuffle=True):
|
| 100 |
"""A map-style torch Dataset of (input_ids, labels, attention_mask) with an exact resume cursor.
|
| 101 |
|
|
|
|
| 107 |
|
| 108 |
n_samples = store.total_tokens // seq_len
|
| 109 |
perm = None
|
| 110 |
+
order_sha = "unshuffled"
|
| 111 |
if shuffle:
|
| 112 |
# Seed must be identical on every resume or the two runs read different data orders. Stored in
|
| 113 |
+
# the checkpoint (cursor.json) and asserted by the trainer -- alongside perm_sha(), which catches
|
| 114 |
+
# the case the seed alone cannot: the same seed on a different numpy.
|
| 115 |
+
base = np.random.default_rng(np.random.PCG64(seed)).permutation(n_samples)
|
| 116 |
+
order_sha = perm_sha(n_samples, seed, base=base)
|
| 117 |
+
perm = base[start_sample:]
|
| 118 |
else:
|
| 119 |
perm = np.arange(start_sample, n_samples, dtype=np.int64)
|
| 120 |
if count is not None:
|
|
|
|
| 136 |
# these names keeps it from dropping anything.
|
| 137 |
column_names = ["input_ids", "labels", "attention_mask"]
|
| 138 |
|
| 139 |
+
ds = _DS()
|
| 140 |
+
# Instance attributes, not class attributes: `_DS.perm = perm` would let a second make_dataset() call
|
| 141 |
+
# in the same process overwrite the first one's order.
|
| 142 |
+
ds.perm = perm
|
| 143 |
+
ds.order_sha = order_sha
|
| 144 |
+
return ds
|
| 145 |
|
| 146 |
|
| 147 |
+
def cursor_dict(seq_len, samples_consumed, seed, dataset_files_sha=None, step=None, order_sha=None):
|
| 148 |
"""The whole of the data-position state. Written into every checkpoint, checked on every resume.
|
| 149 |
|
| 150 |
`step` is recorded alongside `samples_consumed` so the two can be cross-checked on resume: they are
|
| 151 |
redundant by construction (samples = step x sequences-per-step), and a cursor where they disagree is a
|
| 152 |
+
corrupted or hand-edited file, not a position to continue from. `shuffle_perm_sha` is the identity of
|
| 153 |
+
the visit order, which the seed alone does not pin (see perm_sha).
|
| 154 |
"""
|
| 155 |
return {"seq_len": seq_len, "samples_consumed": samples_consumed,
|
| 156 |
"tokens_consumed": samples_consumed * seq_len, "shuffle_seed": seed,
|
| 157 |
+
"shuffle_perm_sha": order_sha, "step": step, "dataset_files_sha": dataset_files_sha}
|
| 158 |
|
| 159 |
|
| 160 |
def load_cursor(dirpath):
|