File size: 10,655 Bytes
9f818c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: OpenMDW-1.1

"""Shared utilities for Action dataset export and evaluation workflows."""

import base64
import fcntl
import json
import os
import subprocess
from pathlib import Path
from typing import Any
from urllib.parse import urlparse

from loguru import logger
from tqdm import tqdm

DEFAULT_GCS_CREDENTIALS = "credentials/gcp_checkpoint.secret"
GCS_CREDS_ENV_VAR = "COSMOS_GCP_CHECKPOINT_CREDS"


# ---------------------------------------------------------------------------
# GCS download utilities (uses s5cmd via uvx for parallel transfers)
# ---------------------------------------------------------------------------


def _parse_s3_uri(uri: str) -> tuple[str, str]:
    """Parse an S3-compatible URI into (bucket, key_prefix)."""
    parsed = urlparse(uri)
    bucket = parsed.netloc
    prefix = parsed.path.strip("/")
    return bucket, prefix


def _to_s3_uri(uri: str) -> str:
    """Normalize a gs:// or s3:// URI to the s3:// scheme expected by s5cmd."""
    bucket, prefix = _parse_s3_uri(uri)
    return f"s3://{bucket}/{prefix}"


def _load_gcs_credentials(credentials_path: str | Path) -> dict[str, Any]:
    """Load GCS S3-compatible credentials from the COSMOS_GCP_CHECKPOINT_CREDS
    env var (base64-encoded JSON) or fall back to a local file."""
    env_value = os.environ.get(GCS_CREDS_ENV_VAR)
    if env_value:
        return json.loads(base64.b64decode(env_value))
    with open(credentials_path) as f:
        return json.load(f)


def _count_remote_objects(
    s3_uri: str,
    env: dict[str, str],
    endpoint_url: str,
) -> int | None:
    """Count remote objects under *s3_uri* using ``s5cmd ls``.

    Returns ``None`` if the listing fails so callers can fall back to an
    indeterminate progress bar.
    """
    cmd = ["uvx", "s5cmd", "--endpoint-url", endpoint_url, "ls", f"{s3_uri}/*"]
    result = subprocess.run(cmd, env=env, capture_output=True, text=True)
    if result.returncode != 0:
        return None
    return sum(1 for line in result.stdout.splitlines() if line.strip())


def _download_with_s5cmd(
    gcs_uri: str,
    local_dir: Path,
    credentials_path: str | Path,
) -> None:
    """Download all objects under S3-compatible URI using s5cmd via uvx.

    s5cmd performs massively parallel transfers and natively skips files that
    already exist at the destination with matching size (via ``sync``).
    """
    creds = _load_gcs_credentials(credentials_path)
    s3_uri = _to_s3_uri(gcs_uri)

    env = {
        **os.environ,
        "AWS_ACCESS_KEY_ID": creds["aws_access_key_id"],
        "AWS_SECRET_ACCESS_KEY": creds["aws_secret_access_key"],
        "AWS_REGION": creds.get("region_name", ""),
    }

    local_dir.mkdir(parents=True, exist_ok=True)

    endpoint_url = creds["endpoint_url"]
    cmd = [
        "uvx",
        "s5cmd",
        "--endpoint-url",
        endpoint_url,
        "sync",
        f"{s3_uri}/*",
        str(local_dir) + "/",
    ]

    logger.info(f"Running: {' '.join(cmd)}")

    total = _count_remote_objects(s3_uri, env, endpoint_url)
    if total is not None:
        logger.info(f"Remote listing: {total} object(s)")

    proc = subprocess.Popen(cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
    assert proc.stdout is not None
    stderr_lines: list[str] = []
    with tqdm(total=total, desc="s5cmd sync", unit="file", dynamic_ncols=True) as pbar:
        for line in proc.stdout:
            if line.strip():
                pbar.update(1)
        proc.wait()
        if proc.stderr:
            stderr_lines = proc.stderr.readlines()

    if proc.returncode != 0:
        raise RuntimeError(f"s5cmd failed (exit {proc.returncode}):\n{''.join(stderr_lines)}")
    logger.success(f"s5cmd sync complete: {s3_uri} -> {local_dir}")


def _extract_lz4_archives(root_dir: Path, dataset_name_override: str = "") -> None:
    """Extract ``.zarr.tar.lz4`` archives found under *root_dir*.

    Each archive ``<name>.zarr.tar.lz4`` is extracted into a sibling
    ``<name>.zarr`` directory.  Already-extracted archives (where the
    target directory exists and is non-empty) are skipped.

    A per-prefix ``.lz4_extract_done.<prefix>`` marker is written after
    extraction so subsequent calls with the same ``dataset_name_override``
    skip the expensive ``rglob`` scan entirely.
    """
    if dataset_name_override:
        prefixes = sorted(name.strip() for name in dataset_name_override.split(",") if name.strip())
        marker_key = "_".join(p.replace("/", "--") for p in prefixes)
        extract_marker = root_dir / f".lz4_extract_done.{marker_key}"
    else:
        prefixes = []
        extract_marker = root_dir / ".lz4_extract_done"

    if extract_marker.exists():
        return

    archives = list(root_dir.rglob("*.zarr.tar.lz4"))
    if not archives:
        extract_marker.touch()
        return

    if prefixes:
        filtered = [a for a in archives if any(str(a.relative_to(root_dir)).startswith(p) for p in prefixes)]
        logger.info(
            f"Filtered {len(archives)} archive(s) to {len(filtered)} matching "
            f"dataset_name_override prefixes: {prefixes}"
        )
        archives = filtered

    if not archives:
        extract_marker.touch()
        return
    logger.info(f"Found {len(archives)} .zarr.tar.lz4 archive(s) to extract under {root_dir}")
    for archive in archives:
        zarr_dir = archive.with_name(archive.name.removesuffix(".tar.lz4"))
        done_marker = zarr_dir.with_suffix(zarr_dir.suffix + ".extract_done")
        if done_marker.exists():
            logger.debug(f"Already extracted: {zarr_dir}")
            continue
        zarr_dir.mkdir(parents=True, exist_ok=True)
        logger.info(f"Extracting {archive.name} -> {zarr_dir}")
        result = subprocess.run(
            f"lz4 -d -c {archive} | tar xf - -C {zarr_dir} --strip-components=1 --overwrite",
            shell=True,
            capture_output=True,
            text=True,
        )
        if result.returncode != 0:
            raise RuntimeError(f"lz4/tar extraction failed for {archive}:\n{result.stderr}")
        done_marker.touch()
    logger.success(f"All archives extracted under {root_dir}")
    extract_marker.touch()


def download_from_gcs_uri(
    gcs_uri: str,
    cache_dir: Path,
    gcs_credentials: str = DEFAULT_GCS_CREDENTIALS,
    force_download: bool = False,
    dataset_name_override: str = "",
) -> Path:
    """Download a dataset from a GCS S3-compatible URI and return the local path."""
    bucket, prefix = _parse_s3_uri(gcs_uri)
    local_dataset_dir = cache_dir / bucket / prefix
    complete_marker = local_dataset_dir / ".download_complete"
    lock_path = Path(str(local_dataset_dir) + ".lock")
    lock_path.parent.mkdir(parents=True, exist_ok=True)

    # Serialize concurrent workers so only one syncs; others wait and reuse.
    # The sentinel file is the source of truth for completeness.
    with open(lock_path, "w") as lock_f:
        fcntl.flock(lock_f, fcntl.LOCK_EX)
        try:
            if not force_download and complete_marker.exists():
                logger.info(f"Dataset already exists at {local_dataset_dir}, skipping download")
            else:
                logger.info(f"Downloading dataset from {gcs_uri} to {local_dataset_dir}")
                local_dataset_dir.mkdir(parents=True, exist_ok=True)
                _download_with_s5cmd(gcs_uri, local_dataset_dir, gcs_credentials)
                _extract_lz4_archives(local_dataset_dir, dataset_name_override=dataset_name_override)
                complete_marker.touch()
        finally:
            fcntl.flock(lock_f, fcntl.LOCK_UN)
    return local_dataset_dir


# ---------------------------------------------------------------------------
# Dataset helpers
# ---------------------------------------------------------------------------


def set_dataset_mode(dataset: Any, mode: str) -> None:
    """Propagate *mode* through wrapper layers to the underlying dataset(s)."""
    if hasattr(dataset, "_datasets"):
        for entry in dataset._datasets:
            if isinstance(entry, dict):
                ds = entry.get("dataset")
            elif isinstance(entry, (list, tuple)):
                ds = entry[1] if len(entry) >= 2 else None
            else:
                ds = entry
            if ds is not None:
                set_dataset_mode(ds, mode)
    if hasattr(dataset, "datasets") and isinstance(dataset.datasets, dict):
        for ds in dataset.datasets.values():
            set_dataset_mode(ds, mode)
    if hasattr(dataset, "dataset"):
        set_dataset_mode(dataset.dataset, mode)
    if hasattr(dataset, "mode"):
        dataset.mode = mode
    if hasattr(dataset, "_mode"):
        dataset._mode = mode


def _apply_root_to_target(target: dict[str, Any], root: str | list[str]) -> None:
    """Set *root* on a single dataset config dict."""
    target["root"] = root
    logger.info(f"Overriding dataset root to {target['root']}")


def override_dataset_root(dataset_config: dict[str, Any], root: str | list[str]) -> None:
    """Inject a *root* override into the Hydra dataset config dict.

    The config is expected to be a ``wrap_dataset(list_of_datasets=<inner>)``
    structure.  We set ``root`` on the inner dataset config so the dataset
    class uses the provided local path instead of its default.

    When *root* is a list it is used directly (one root per repo_id).
    When *root* is a single string and the existing config has a list of roots,
    the single value is broadcast to every entry.

    When a single *root* string is given we also search for ``*normalizer*.json``
    files under that directory and, if found, set ``normalizer_dir`` on the inner
    config so that datasets whose normalizer was downloaded alongside the data
    (e.g. UMI from GCS) pick up the local copy instead of the original
    cluster-local path baked into the Hydra YAML.

    ``list_of_datasets`` may be a dict (single / narrowed entry) or a list of
    dataset-entry dicts.  In the list case we apply the override to each entry.
    """
    inner = dataset_config.get("list_of_datasets", dataset_config)

    if isinstance(inner, list):
        for entry in inner:
            if isinstance(entry, dict):
                target = entry.get("dataset", entry)
                if isinstance(target, dict):
                    _apply_root_to_target(target, root)
    elif isinstance(inner, dict):
        _apply_root_to_target(inner, root)
    else:
        _apply_root_to_target(dataset_config, root)