File size: 6,869 Bytes
15c6b87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f37a2c
 
 
 
 
 
 
 
 
15c6b87
9f37a2c
 
15c6b87
 
9f37a2c
 
15c6b87
9f37a2c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15c6b87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e8c3967
 
 
 
 
15c6b87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# Hub sync for the mix build: the instance is scratch, so nothing that took an hour to produce may live
# only there (§3.13 "never let the instance be the only copy", applied to a CPU job rather than a
# checkpoint).
#
# Granularity is one *source*: staging a source takes 5-40 minutes, so publishing its shards the moment
# it finishes bounds the loss from any interruption to the one source in flight, and a resume can skip
# every source already on the Hub without re-reading it.
#
# The stage repo is private-in-practice but public by project rule; it holds no credentials and no
# benchmark material -- only token ids from the approved mix.

import json
import os


def repo_id_from_env(default=None):
    return os.environ.get("OUNCE100M_STAGE_REPO") or default


def api():
    import os

    import ounce100m_credentials

    ounce100m_credentials.install()
    from huggingface_hub import HfApi

    return HfApi(token=os.environ["HF_TOKEN"])


def ensure_repo(repo):
    # Must install() first: the module-level create_repo() resolves its token from the environment, and
    # this is the FIRST hub call a build makes -- before any api() has run. Calling it unauthenticated
    # is what produced the misleading 401 in the cold-resume rehearsal (E-017), and the diagnosis in
    # these notes was wrong about the cause until p2-token-scope proved dataset writes are permitted.
    import os

    api()
    from huggingface_hub import create_repo

    # hub 1.11 (the image's version) takes the owner inside repo_id and has no `namespace` kwarg (E-013)
    create_repo(repo_id=repo, repo_type="dataset", private=False, exist_ok=True,
                token=os.environ["HF_TOKEN"])
    return repo


class StageListingError(RuntimeError):
    """Raised when the stage repo's contents cannot be determined.



    Returning an empty set here would be the single most dangerous shortcut in this pipeline: an empty

    listing is indistinguishable from "nothing staged yet", so a *broken* listing makes a resume

    silently re-stage everything (wasting hours) or, worse, makes a merge believe there is nothing to

    merge while still looking consistent. Callers must see the difference between empty and unknown."""


def remote_stage_files(repo):
    """Relative paths under stage/ that exist on the Hub. Raises rather than returning {} on failure."""
    import json
    import urllib.request

    errors = []
    a = api()
    try:
        files = a.list_repo_files(repo_id=repo, repo_type="dataset", revision="main")
        return {f for f in files if f.startswith("stage/")}
    except Exception as e:
        errors.append(f"list_repo_files: {type(e).__name__}: {str(e)[:160]}")
        # A freshly created repo has no commits, so "main" does not resolve and list_repo_files raises
        # for a reason that genuinely means empty. Distinguish it by asking whether the repo exists at
        # all -- NOT by swallowing the exception, which is what made the first version ambiguous.
        try:
            info = a.repo_info(repo_id=repo, repo_type="dataset")
            if not getattr(info, "sha", None):
                return set()
        except Exception as e2:
            errors.append(f"repo_info: {type(e2).__name__}: {str(e2)[:120]}")

    # REST fallback. Note the path goes in the query string, not the URL body -- the first version put
    # it in the path, got a 404, and the bare `except` turned that into a silent empty listing (E-018).
    for url in (f"https://huggingface.co/api/datasets/{repo}/tree/main?recursive=true",
                f"https://huggingface.co/api/datasets/{repo}/tree/main"):
        try:
            with urllib.request.urlopen(url, timeout=90) as r:
                entries = json.loads(r.read().decode())
            paths = set()
            stack = [(e, "") for e in entries]
            while stack:
                e, prefix = stack.pop()
                p = f"{prefix}/{e['path']}".lstrip("/") if prefix else e.get("path", "")
                if e.get("type") == "dir":
                    stack.extend((c, p) for c in (e.get("children") or []))
                elif p.startswith("stage/"):
                    paths.add(p)
            return paths
        except Exception as e:
            errors.append(f"tree {url.split('/api/')[1]}: {type(e).__name__}: {str(e)[:140]}")

    raise StageListingError(f"cannot list {repo}: " + " | ".join(errors))


def upload_source(root, key, repo):
    """Publish stage/<key>/* in one commit. Returns the file names pushed."""
    a = api()
    local = os.path.join(root, "stage", key)
    files = sorted(f for f in os.listdir(local) if f.endswith((".bin", ".json")))
    if not files:
        return []
    a.upload_folder(folder_path=local, path_in_repo=f"stage/{key}", repo_id=repo,
                    repo_type="dataset", commit_message=f"stage {key}: {len(files)} shard(s)",
                    allow_patterns=["*.bin", "*.json"])
    return files


def download_missing(root, keys_wanted, repo, remote=None):
    """Fetch stage/<key>/*.bin for every wanted key not already on disk. Returns {key: [filenames]}."""
    from huggingface_hub import hf_hub_download

    remote = remote if remote is not None else remote_stage_files(repo)
    got = {}
    for key in keys_wanted:
        # .json must come down too: record.json is what tells the restore the shard list, token count
        # and drop stats. A .bin-only filter made every restored source look unknown and was the actual
        # cause of "restored 0 staged source(s)" in rehearsal v3, with the files plainly present.
        want = sorted(p for p in remote
                      if p.startswith(f"stage/{key}/") and p.endswith((".bin", ".json")))
        if not want:
            got[key] = []
            continue
        local_dir = os.path.join(root, "stage", key)
        os.makedirs(local_dir, exist_ok=True)
        have = {f"stage/{key}/{f}" for f in os.listdir(local_dir)}
        for rp in want:
            if rp in have:
                continue
            hf_hub_download(repo_id=repo, repo_type="dataset", filename=rp,
                            local_dir=root, force_download=False)
        got[key] = sorted(os.listdir(local_dir))
    return got


def status(repo):
    import urllib.request

    url = f"https://huggingface.co/api/datasets/{repo}"
    try:
        with urllib.request.urlopen(url, timeout=60) as r:
            d = json.loads(r.read().decode())
        return {"exists": True, "gated": d.get("gated"), "private": d.get("private"),
                "lastModified": d.get("lastModified"), "id": d.get("id")}
    except Exception as e:
        return {"exists": False, "error": f"{type(e).__name__}: {str(e)[:120]}"}