publish_mix: resume listing must read lfs.oid as a dict (getattr on a dict returned None for every shard, so a 'resume' re-sent all 2.3 GB); hard-exit 11/12 when a batched commit leaves a path absent or a remote sha that disagrees with the local bytes
Browse files- build/publish_mix.py +63 -14
build/publish_mix.py
CHANGED
|
@@ -117,6 +117,27 @@ def attribution(man):
|
|
| 117 |
return ATTRIBUTION + "\n".join(lines) + "\n"
|
| 118 |
|
| 119 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
def main():
|
| 121 |
ap = argparse.ArgumentParser()
|
| 122 |
ap.add_argument("--root", default="/kaggle/working/mixroot")
|
|
@@ -235,30 +256,29 @@ def main():
|
|
| 235 |
# because a bare upload_file has no retry and no place to resume from).
|
| 236 |
remote_sha = {}
|
| 237 |
try:
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
for ent in api.dataset_info(a.repo, files_metadata=True).siblings or []:
|
| 241 |
-
remote_sha[ent.rfilename] = getattr(ent, "lfs", None) and getattr(ent.lfs, "sha256", None) \
|
| 242 |
-
or getattr(ent, "sha256", None)
|
| 243 |
except Exception as e:
|
| 244 |
print(f" (no resumable listing: {type(e).__name__}; uploading everything)", flush=True)
|
| 245 |
|
| 246 |
-
failures = []
|
| 247 |
todo = []
|
| 248 |
for np, lp in to_upload:
|
|
|
|
| 249 |
with open(lp, "rb") as fh:
|
| 250 |
-
|
| 251 |
-
|
|
|
|
| 252 |
if remote_sha.get(np) == local_sha:
|
| 253 |
print(f" = {np} already on the Hub with this sha256, skipped", flush=True)
|
| 254 |
continue
|
| 255 |
-
todo.append((np, lp, local_sha
|
| 256 |
print(f" {len(todo)} of {len(to_upload)} files still need uploading", flush=True)
|
| 257 |
|
| 258 |
# ONE commit for the whole batch, not one commit per file. Gate 2f proved why: 139 sequential
|
| 259 |
# `upload_file` commits succeeded and then the Hub began rejecting every request -- including 30-byte
|
| 260 |
# files -- with a bare HfHubHTTPError, which is a commit-rate limit, not a network fault. A single
|
| 261 |
# upload_folder call is one commit and handles LFS itself.
|
|
|
|
| 262 |
if todo:
|
| 263 |
try:
|
| 264 |
api.upload_folder(repo_id=a.repo, repo_type="dataset", folder_path=a.root, path_in_repo="",
|
|
@@ -270,9 +290,11 @@ def main():
|
|
| 270 |
except Exception as e:
|
| 271 |
print(f" batched commit failed ({type(e).__name__}: {str(e)[:160]}); "
|
| 272 |
"falling back to per-file uploads with retries", flush=True)
|
| 273 |
-
for np, lp, local_sha
|
| 274 |
for attempt in (1, 2, 3):
|
| 275 |
try:
|
|
|
|
|
|
|
| 276 |
api.upload_file(path_or_fileobj=payload, path_in_repo=np, repo_id=a.repo,
|
| 277 |
repo_type="dataset",
|
| 278 |
commit_message=f"add {np} ({local_sha[:12]})")
|
|
@@ -290,11 +312,30 @@ def main():
|
|
| 290 |
"resumes from the ones that did", flush=True)
|
| 291 |
sys.exit(11)
|
| 292 |
|
| 293 |
-
# Independent verification from the Hub, not from the client that just wrote it.
|
|
|
|
|
|
|
| 294 |
remote = set(api.list_repo_files(repo_id=a.repo, repo_type="dataset"))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
want = {np for np, _ in to_upload} | {".gitattributes"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
R = {"repo": a.repo, "files_expected": len(want), "files_present": len(want & remote),
|
| 297 |
-
"missing": sorted(want - remote)
|
|
|
|
|
|
|
|
|
|
| 298 |
first = man["shards"][0]["file"]
|
| 299 |
url = f"https://huggingface.co/datasets/{a.repo}/resolve/main/{first}"
|
| 300 |
try:
|
|
@@ -307,11 +348,19 @@ def main():
|
|
| 307 |
== man["shards"][0]["sha256"]}
|
| 308 |
except Exception as e:
|
| 309 |
R["anonymous_shard_read"] = {"error": f"{type(e).__name__}: {str(e)[:160]}"}
|
| 310 |
-
R["published_verified"] = bool(not R["missing"] and R["
|
| 311 |
-
|
| 312 |
print("PUBLISH_JSON_BEGIN")
|
| 313 |
print(json.dumps(R, indent=1, default=str))
|
| 314 |
print("PUBLISH_JSON_END")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
|
| 316 |
|
| 317 |
if __name__ == "__main__":
|
|
|
|
| 117 |
return ATTRIBUTION + "\n".join(lines) + "\n"
|
| 118 |
|
| 119 |
|
| 120 |
+
def remote_digests(api, repo):
|
| 121 |
+
"""path -> content sha256 for what is already on the Hub, so a re-run resumes instead of resending.
|
| 122 |
+
|
| 123 |
+
Only LFS-tracked files expose a content sha256 (`lfs.oid`, sha256 by construction). Small non-LFS
|
| 124 |
+
files expose only a git blob sha, which is not a content sha256, so they are reported unknown and
|
| 125 |
+
re-uploaded -- a handful of json/md/u8 files, which costs nothing. `lfs` arrives as a dict from
|
| 126 |
+
huggingface_hub, so attribute access alone silently returns None for every shard and turns a resume
|
| 127 |
+
into a full 2.3 GB re-upload (Gate 2f).
|
| 128 |
+
"""
|
| 129 |
+
out = {}
|
| 130 |
+
for ent in api.dataset_info(repo, repo_type="dataset", files_metadata=True).siblings or []:
|
| 131 |
+
lfs = getattr(ent, "lfs", None)
|
| 132 |
+
if isinstance(lfs, dict):
|
| 133 |
+
out[ent.rfilename] = lfs.get("sha256") or lfs.get("oid")
|
| 134 |
+
elif lfs is not None:
|
| 135 |
+
out[ent.rfilename] = getattr(lfs, "sha256", None) or getattr(lfs, "oid", None)
|
| 136 |
+
else:
|
| 137 |
+
out[ent.rfilename] = None
|
| 138 |
+
return out
|
| 139 |
+
|
| 140 |
+
|
| 141 |
def main():
|
| 142 |
ap = argparse.ArgumentParser()
|
| 143 |
ap.add_argument("--root", default="/kaggle/working/mixroot")
|
|
|
|
| 256 |
# because a bare upload_file has no retry and no place to resume from).
|
| 257 |
remote_sha = {}
|
| 258 |
try:
|
| 259 |
+
remote_sha = remote_digests(api, a.repo)
|
| 260 |
+
print(f" {len(remote_sha)} file(s) already on the Hub", flush=True)
|
|
|
|
|
|
|
|
|
|
| 261 |
except Exception as e:
|
| 262 |
print(f" (no resumable listing: {type(e).__name__}; uploading everything)", flush=True)
|
| 263 |
|
|
|
|
| 264 |
todo = []
|
| 265 |
for np, lp in to_upload:
|
| 266 |
+
h = hashlib.sha256()
|
| 267 |
with open(lp, "rb") as fh:
|
| 268 |
+
for chunk in iter(lambda: fh.read(1 << 20), b""):
|
| 269 |
+
h.update(chunk)
|
| 270 |
+
local_sha = h.hexdigest()
|
| 271 |
if remote_sha.get(np) == local_sha:
|
| 272 |
print(f" = {np} already on the Hub with this sha256, skipped", flush=True)
|
| 273 |
continue
|
| 274 |
+
todo.append((np, lp, local_sha))
|
| 275 |
print(f" {len(todo)} of {len(to_upload)} files still need uploading", flush=True)
|
| 276 |
|
| 277 |
# ONE commit for the whole batch, not one commit per file. Gate 2f proved why: 139 sequential
|
| 278 |
# `upload_file` commits succeeded and then the Hub began rejecting every request -- including 30-byte
|
| 279 |
# files -- with a bare HfHubHTTPError, which is a commit-rate limit, not a network fault. A single
|
| 280 |
# upload_folder call is one commit and handles LFS itself.
|
| 281 |
+
failures = []
|
| 282 |
if todo:
|
| 283 |
try:
|
| 284 |
api.upload_folder(repo_id=a.repo, repo_type="dataset", folder_path=a.root, path_in_repo="",
|
|
|
|
| 290 |
except Exception as e:
|
| 291 |
print(f" batched commit failed ({type(e).__name__}: {str(e)[:160]}); "
|
| 292 |
"falling back to per-file uploads with retries", flush=True)
|
| 293 |
+
for np, lp, local_sha in todo:
|
| 294 |
for attempt in (1, 2, 3):
|
| 295 |
try:
|
| 296 |
+
with open(lp, "rb") as fh:
|
| 297 |
+
payload = fh.read()
|
| 298 |
api.upload_file(path_or_fileobj=payload, path_in_repo=np, repo_id=a.repo,
|
| 299 |
repo_type="dataset",
|
| 300 |
commit_message=f"add {np} ({local_sha[:12]})")
|
|
|
|
| 312 |
"resumes from the ones that did", flush=True)
|
| 313 |
sys.exit(11)
|
| 314 |
|
| 315 |
+
# Independent verification from the Hub, not from the client that just wrote it. A batched commit can
|
| 316 |
+
# return without raising and still not carry every file, so presence is checked per path and the sha
|
| 317 |
+
# is re-required wherever the Hub exposes one (LFS).
|
| 318 |
remote = set(api.list_repo_files(repo_id=a.repo, repo_type="dataset"))
|
| 319 |
+
try:
|
| 320 |
+
after = remote_digests(api, a.repo)
|
| 321 |
+
except Exception:
|
| 322 |
+
after = {}
|
| 323 |
want = {np for np, _ in to_upload} | {".gitattributes"}
|
| 324 |
+
local_by_path = {np: lp for np, lp in to_upload}
|
| 325 |
+
sha_bad = []
|
| 326 |
+
for np, rsha in after.items():
|
| 327 |
+
if rsha and np in local_by_path:
|
| 328 |
+
h = hashlib.sha256()
|
| 329 |
+
with open(local_by_path[np], "rb") as fh:
|
| 330 |
+
for chunk in iter(lambda: fh.read(1 << 20), b""):
|
| 331 |
+
h.update(chunk)
|
| 332 |
+
if h.hexdigest() != rsha:
|
| 333 |
+
sha_bad.append(np)
|
| 334 |
R = {"repo": a.repo, "files_expected": len(want), "files_present": len(want & remote),
|
| 335 |
+
"missing": sorted(want - remote), "sha_mismatch_on_hub": sorted(sha_bad)[:20],
|
| 336 |
+
"sha_verified_files": sum(1 for v in after.values() if v)}
|
| 337 |
+
print("after upload: %d/%d present, %d missing, %d sha mismatch" % (
|
| 338 |
+
R["files_present"], R["files_expected"], len(R["missing"]), len(sha_bad)), flush=True)
|
| 339 |
first = man["shards"][0]["file"]
|
| 340 |
url = f"https://huggingface.co/datasets/{a.repo}/resolve/main/{first}"
|
| 341 |
try:
|
|
|
|
| 348 |
== man["shards"][0]["sha256"]}
|
| 349 |
except Exception as e:
|
| 350 |
R["anonymous_shard_read"] = {"error": f"{type(e).__name__}: {str(e)[:160]}"}
|
| 351 |
+
R["published_verified"] = bool(not R["missing"] and not R["sha_mismatch_on_hub"]
|
| 352 |
+
and R["anonymous_shard_read"].get("sha256_matches_manifest"))
|
| 353 |
print("PUBLISH_JSON_BEGIN")
|
| 354 |
print(json.dumps(R, indent=1, default=str))
|
| 355 |
print("PUBLISH_JSON_END")
|
| 356 |
+
if not R["missing"] and R["sha_mismatch_on_hub"]:
|
| 357 |
+
print(f"FATAL: {len(R['sha_mismatch_on_hub'])} file(s) on the Hub do not match the local bytes",
|
| 358 |
+
flush=True)
|
| 359 |
+
sys.exit(12)
|
| 360 |
+
if R["missing"]:
|
| 361 |
+
print(f"FATAL: {len(R['missing'])} file(s) still absent after the commit -- re-running "
|
| 362 |
+
"publish_mix resumes from the ones that landed", flush=True)
|
| 363 |
+
sys.exit(11)
|
| 364 |
|
| 365 |
|
| 366 |
if __name__ == "__main__":
|