wandb 0.15.9__py3-none-any.whl → 0.15.11__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- wandb/__init__.py +5 -1
- wandb/apis/public.py +137 -17
- wandb/apis/reports/_panels.py +1 -1
- wandb/apis/reports/blocks.py +1 -0
- wandb/apis/reports/report.py +27 -5
- wandb/cli/cli.py +52 -41
- wandb/docker/__init__.py +17 -0
- wandb/docker/auth.py +1 -1
- wandb/env.py +24 -4
- wandb/filesync/step_checksum.py +3 -3
- wandb/integration/openai/openai.py +3 -0
- wandb/integration/ultralytics/__init__.py +9 -0
- wandb/integration/ultralytics/bbox_utils.py +196 -0
- wandb/integration/ultralytics/callback.py +458 -0
- wandb/integration/ultralytics/classification_utils.py +66 -0
- wandb/integration/ultralytics/mask_utils.py +141 -0
- wandb/integration/ultralytics/pose_utils.py +92 -0
- wandb/integration/xgboost/xgboost.py +3 -3
- wandb/integration/yolov8/__init__.py +0 -7
- wandb/integration/yolov8/yolov8.py +22 -3
- wandb/old/settings.py +7 -0
- wandb/plot/line_series.py +0 -1
- wandb/proto/v3/wandb_internal_pb2.py +353 -300
- wandb/proto/v3/wandb_server_pb2.py +37 -41
- wandb/proto/v3/wandb_settings_pb2.py +2 -2
- wandb/proto/v3/wandb_telemetry_pb2.py +16 -16
- wandb/proto/v4/wandb_internal_pb2.py +272 -260
- wandb/proto/v4/wandb_server_pb2.py +37 -40
- wandb/proto/v4/wandb_settings_pb2.py +2 -2
- wandb/proto/v4/wandb_telemetry_pb2.py +16 -16
- wandb/proto/wandb_internal_codegen.py +7 -31
- wandb/sdk/artifacts/artifact.py +321 -189
- wandb/sdk/artifacts/artifact_cache.py +14 -0
- wandb/sdk/artifacts/artifact_manifest.py +5 -4
- wandb/sdk/artifacts/artifact_manifest_entry.py +37 -9
- wandb/sdk/artifacts/artifact_manifests/artifact_manifest_v1.py +1 -9
- wandb/sdk/artifacts/artifact_saver.py +13 -50
- wandb/sdk/artifacts/artifact_ttl.py +6 -0
- wandb/sdk/artifacts/artifacts_cache.py +119 -93
- wandb/sdk/artifacts/staging.py +25 -0
- wandb/sdk/artifacts/storage_handlers/s3_handler.py +12 -7
- wandb/sdk/artifacts/storage_handlers/wb_local_artifact_handler.py +2 -3
- wandb/sdk/artifacts/storage_policies/__init__.py +4 -0
- wandb/sdk/artifacts/storage_policies/register.py +1 -0
- wandb/sdk/artifacts/storage_policies/wandb_storage_policy.py +4 -3
- wandb/sdk/artifacts/storage_policy.py +4 -2
- wandb/sdk/backend/backend.py +0 -16
- wandb/sdk/data_types/image.py +3 -1
- wandb/sdk/integration_utils/auto_logging.py +38 -13
- wandb/sdk/interface/interface.py +16 -135
- wandb/sdk/interface/interface_shared.py +9 -147
- wandb/sdk/interface/interface_sock.py +0 -26
- wandb/sdk/internal/file_pusher.py +20 -3
- wandb/sdk/internal/file_stream.py +3 -1
- wandb/sdk/internal/handler.py +53 -70
- wandb/sdk/internal/internal_api.py +220 -130
- wandb/sdk/internal/job_builder.py +41 -37
- wandb/sdk/internal/sender.py +7 -25
- wandb/sdk/internal/system/assets/disk.py +144 -11
- wandb/sdk/internal/system/system_info.py +6 -2
- wandb/sdk/launch/__init__.py +5 -0
- wandb/sdk/launch/{launch.py → _launch.py} +53 -54
- wandb/sdk/launch/{launch_add.py → _launch_add.py} +34 -31
- wandb/sdk/launch/_project_spec.py +13 -2
- wandb/sdk/launch/agent/agent.py +103 -59
- wandb/sdk/launch/agent/run_queue_item_file_saver.py +6 -4
- wandb/sdk/launch/builder/build.py +19 -1
- wandb/sdk/launch/builder/docker_builder.py +5 -1
- wandb/sdk/launch/builder/kaniko_builder.py +5 -1
- wandb/sdk/launch/create_job.py +20 -5
- wandb/sdk/launch/loader.py +14 -5
- wandb/sdk/launch/runner/abstract.py +0 -2
- wandb/sdk/launch/runner/kubernetes_monitor.py +329 -0
- wandb/sdk/launch/runner/kubernetes_runner.py +66 -209
- wandb/sdk/launch/runner/local_container.py +5 -2
- wandb/sdk/launch/runner/local_process.py +4 -1
- wandb/sdk/launch/sweeps/scheduler.py +43 -25
- wandb/sdk/launch/sweeps/utils.py +5 -3
- wandb/sdk/launch/utils.py +3 -1
- wandb/sdk/lib/_settings_toposort_generate.py +3 -9
- wandb/sdk/lib/_settings_toposort_generated.py +27 -3
- wandb/sdk/lib/_wburls_generated.py +1 -0
- wandb/sdk/lib/filenames.py +27 -6
- wandb/sdk/lib/filesystem.py +181 -7
- wandb/sdk/lib/fsm.py +5 -3
- wandb/sdk/lib/gql_request.py +3 -0
- wandb/sdk/lib/ipython.py +7 -0
- wandb/sdk/lib/wburls.py +1 -0
- wandb/sdk/service/port_file.py +2 -15
- wandb/sdk/service/server.py +7 -55
- wandb/sdk/service/service.py +56 -26
- wandb/sdk/service/service_base.py +1 -1
- wandb/sdk/service/streams.py +11 -5
- wandb/sdk/verify/verify.py +2 -2
- wandb/sdk/wandb_init.py +8 -2
- wandb/sdk/wandb_manager.py +4 -14
- wandb/sdk/wandb_run.py +143 -53
- wandb/sdk/wandb_settings.py +148 -35
- wandb/testing/relay.py +85 -38
- wandb/util.py +87 -4
- wandb/wandb_torch.py +24 -38
- {wandb-0.15.9.dist-info → wandb-0.15.11.dist-info}/METADATA +48 -23
- {wandb-0.15.9.dist-info → wandb-0.15.11.dist-info}/RECORD +107 -103
- {wandb-0.15.9.dist-info → wandb-0.15.11.dist-info}/WHEEL +1 -1
- wandb/proto/v3/wandb_server_pb2_grpc.py +0 -1422
- wandb/proto/v4/wandb_server_pb2_grpc.py +0 -1422
- wandb/proto/wandb_server_pb2_grpc.py +0 -8
- wandb/sdk/artifacts/storage_policies/s3_bucket_policy.py +0 -61
- wandb/sdk/interface/interface_grpc.py +0 -460
- wandb/sdk/service/server_grpc.py +0 -444
- wandb/sdk/service/service_grpc.py +0 -73
- {wandb-0.15.9.dist-info → wandb-0.15.11.dist-info}/LICENSE +0 -0
- {wandb-0.15.9.dist-info → wandb-0.15.11.dist-info}/entry_points.txt +0 -0
- {wandb-0.15.9.dist-info → wandb-0.15.11.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
"""Recent Artifact storage.
|
2
|
+
|
3
|
+
Artifacts are registered in the cache to ensure they won't be immediately garbage
|
4
|
+
collected and can be retrieved by their ID.
|
5
|
+
"""
|
6
|
+
from typing import TYPE_CHECKING, Dict
|
7
|
+
|
8
|
+
from wandb.sdk.lib.capped_dict import CappedDict
|
9
|
+
|
10
|
+
if TYPE_CHECKING:
|
11
|
+
from wandb.sdk.artifacts.artifact import Artifact
|
12
|
+
|
13
|
+
# There is nothing special about the artifact cache, it's just a global capped dict.
|
14
|
+
artifact_cache: Dict[str, "Artifact"] = CappedDict(100)
|
@@ -5,9 +5,7 @@ from wandb.sdk.lib.hashutil import HexMD5
|
|
5
5
|
|
6
6
|
if TYPE_CHECKING:
|
7
7
|
from wandb.sdk.artifacts.artifact_manifest_entry import ArtifactManifestEntry
|
8
|
-
from wandb.sdk.artifacts.
|
9
|
-
WandbStoragePolicy,
|
10
|
-
)
|
8
|
+
from wandb.sdk.artifacts.storage_policy import StoragePolicy
|
11
9
|
|
12
10
|
|
13
11
|
class ArtifactManifest:
|
@@ -29,12 +27,15 @@ class ArtifactManifest:
|
|
29
27
|
|
30
28
|
def __init__(
|
31
29
|
self,
|
32
|
-
storage_policy: "
|
30
|
+
storage_policy: "StoragePolicy",
|
33
31
|
entries: Optional[Mapping[str, "ArtifactManifestEntry"]] = None,
|
34
32
|
) -> None:
|
35
33
|
self.storage_policy = storage_policy
|
36
34
|
self.entries = dict(entries) if entries else {}
|
37
35
|
|
36
|
+
def __len__(self) -> int:
|
37
|
+
return len(self.entries)
|
38
|
+
|
38
39
|
def to_manifest_json(self) -> Dict:
|
39
40
|
raise NotImplementedError
|
40
41
|
|
@@ -1,11 +1,10 @@
|
|
1
1
|
"""Artifact manifest entry."""
|
2
|
+
import json
|
2
3
|
import os
|
3
4
|
from pathlib import Path
|
4
5
|
from typing import TYPE_CHECKING, Dict, Optional, Union
|
5
6
|
from urllib.parse import urlparse
|
6
7
|
|
7
|
-
import wandb
|
8
|
-
from wandb import util
|
9
8
|
from wandb.errors.term import termwarn
|
10
9
|
from wandb.sdk.lib import filesystem
|
11
10
|
from wandb.sdk.lib.hashutil import (
|
@@ -18,7 +17,6 @@ from wandb.sdk.lib.hashutil import (
|
|
18
17
|
from wandb.sdk.lib.paths import FilePathStr, LogicalPath, StrPath, URIStr
|
19
18
|
|
20
19
|
if TYPE_CHECKING:
|
21
|
-
from wandb.apis.public import RetryingClient
|
22
20
|
from wandb.sdk.artifacts.artifact import Artifact
|
23
21
|
|
24
22
|
|
@@ -56,6 +54,38 @@ class ArtifactManifestEntry:
|
|
56
54
|
if self.local_path and self.size is None:
|
57
55
|
self.size = Path(self.local_path).stat().st_size
|
58
56
|
|
57
|
+
def __repr__(self) -> str:
|
58
|
+
cls = self.__class__.__name__
|
59
|
+
ref = f", ref={self.ref!r}" if self.ref is not None else ""
|
60
|
+
birth_artifact_id = (
|
61
|
+
f", birth_artifact_id={self.birth_artifact_id!r}"
|
62
|
+
if self.birth_artifact_id is not None
|
63
|
+
else ""
|
64
|
+
)
|
65
|
+
size = f", size={self.size}" if self.size is not None else ""
|
66
|
+
extra = f", extra={json.dumps(self.extra)}" if self.extra else ""
|
67
|
+
local_path = f", local_path={self.local_path!r}" if self.local_path else ""
|
68
|
+
others = ref + birth_artifact_id + size + extra + local_path
|
69
|
+
return f"{cls}(path={self.path!r}, digest={self.digest!r}{others})"
|
70
|
+
|
71
|
+
def __eq__(self, other: object) -> bool:
|
72
|
+
"""Strict equality, comparing all public fields.
|
73
|
+
|
74
|
+
ArtifactManifestEntries for the same file may not compare equal if they were
|
75
|
+
added in different ways or created for different parent artifacts.
|
76
|
+
"""
|
77
|
+
if not isinstance(other, ArtifactManifestEntry):
|
78
|
+
return False
|
79
|
+
return (
|
80
|
+
self.path == other.path
|
81
|
+
and self.digest == other.digest
|
82
|
+
and self.ref == other.ref
|
83
|
+
and self.birth_artifact_id == other.birth_artifact_id
|
84
|
+
and self.size == other.size
|
85
|
+
and self.extra == other.extra
|
86
|
+
and self.local_path == other.local_path
|
87
|
+
)
|
88
|
+
|
59
89
|
@property
|
60
90
|
def name(self) -> LogicalPath:
|
61
91
|
# TODO(hugh): add telemetry to see if anyone is still using this.
|
@@ -151,9 +181,7 @@ class ArtifactManifestEntry:
|
|
151
181
|
def _is_artifact_reference(self) -> bool:
|
152
182
|
return self.ref is not None and urlparse(self.ref).scheme == "wandb-artifact"
|
153
183
|
|
154
|
-
def
|
155
|
-
|
156
|
-
|
157
|
-
)
|
158
|
-
assert artifact is not None
|
159
|
-
return artifact
|
184
|
+
def _referenced_artifact_id(self) -> Optional[str]:
|
185
|
+
if not self._is_artifact_reference():
|
186
|
+
return None
|
187
|
+
return hex_to_b64_id(urlparse(self.ref).netloc)
|
@@ -3,7 +3,6 @@ from typing import Any, Dict, Mapping, Optional
|
|
3
3
|
|
4
4
|
from wandb.sdk.artifacts.artifact_manifest import ArtifactManifest
|
5
5
|
from wandb.sdk.artifacts.artifact_manifest_entry import ArtifactManifestEntry
|
6
|
-
from wandb.sdk.artifacts.storage_policies.wandb_storage_policy import WandbStoragePolicy
|
7
6
|
from wandb.sdk.artifacts.storage_policy import StoragePolicy
|
8
7
|
from wandb.sdk.lib.hashutil import HexMD5, _md5
|
9
8
|
|
@@ -23,13 +22,6 @@ class ArtifactManifestV1(ArtifactManifest):
|
|
23
22
|
storage_policy_name = manifest_json["storagePolicy"]
|
24
23
|
storage_policy_config = manifest_json.get("storagePolicyConfig", {})
|
25
24
|
storage_policy_cls = StoragePolicy.lookup_by_name(storage_policy_name)
|
26
|
-
if storage_policy_cls is None:
|
27
|
-
raise ValueError('Failed to find storage policy "%s"' % storage_policy_name)
|
28
|
-
if not issubclass(storage_policy_cls, WandbStoragePolicy):
|
29
|
-
raise ValueError(
|
30
|
-
"No handler found for storage handler of type '%s'"
|
31
|
-
% storage_policy_name
|
32
|
-
)
|
33
25
|
|
34
26
|
entries: Mapping[str, ArtifactManifestEntry]
|
35
27
|
entries = {
|
@@ -49,7 +41,7 @@ class ArtifactManifestV1(ArtifactManifest):
|
|
49
41
|
|
50
42
|
def __init__(
|
51
43
|
self,
|
52
|
-
storage_policy: "
|
44
|
+
storage_policy: "StoragePolicy",
|
53
45
|
entries: Optional[Mapping[str, ArtifactManifestEntry]] = None,
|
54
46
|
) -> None:
|
55
47
|
super().__init__(storage_policy, entries=entries)
|
@@ -4,15 +4,15 @@ import json
|
|
4
4
|
import os
|
5
5
|
import sys
|
6
6
|
import tempfile
|
7
|
-
from typing import TYPE_CHECKING, Awaitable, Dict,
|
7
|
+
from typing import TYPE_CHECKING, Awaitable, Dict, Optional, Sequence
|
8
8
|
|
9
9
|
import wandb
|
10
10
|
import wandb.filesync.step_prepare
|
11
|
-
from wandb import
|
11
|
+
from wandb import util
|
12
12
|
from wandb.sdk.artifacts.artifact_manifest import ArtifactManifest
|
13
|
-
from wandb.sdk.
|
13
|
+
from wandb.sdk.artifacts.staging import get_staging_dir
|
14
14
|
from wandb.sdk.lib.hashutil import B64MD5, b64_to_hex_id, md5_file_b64
|
15
|
-
from wandb.sdk.lib.paths import
|
15
|
+
from wandb.sdk.lib.paths import URIStr
|
16
16
|
|
17
17
|
if TYPE_CHECKING:
|
18
18
|
from wandb.sdk.artifacts.artifact_manifest_entry import ArtifactManifestEntry
|
@@ -65,9 +65,9 @@ class ArtifactSaver:
|
|
65
65
|
distributed_id: Optional[str] = None,
|
66
66
|
finalize: bool = True,
|
67
67
|
metadata: Optional[Dict] = None,
|
68
|
+
ttl_duration_seconds: Optional[int] = None,
|
68
69
|
description: Optional[str] = None,
|
69
70
|
aliases: Optional[Sequence[str]] = None,
|
70
|
-
labels: Optional[List[str]] = None,
|
71
71
|
use_after_commit: bool = False,
|
72
72
|
incremental: bool = False,
|
73
73
|
history_step: Optional[int] = None,
|
@@ -82,9 +82,9 @@ class ArtifactSaver:
|
|
82
82
|
distributed_id,
|
83
83
|
finalize,
|
84
84
|
metadata,
|
85
|
+
ttl_duration_seconds,
|
85
86
|
description,
|
86
87
|
aliases,
|
87
|
-
labels,
|
88
88
|
use_after_commit,
|
89
89
|
incremental,
|
90
90
|
history_step,
|
@@ -102,33 +102,17 @@ class ArtifactSaver:
|
|
102
102
|
distributed_id: Optional[str] = None,
|
103
103
|
finalize: bool = True,
|
104
104
|
metadata: Optional[Dict] = None,
|
105
|
+
ttl_duration_seconds: Optional[int] = None,
|
105
106
|
description: Optional[str] = None,
|
106
107
|
aliases: Optional[Sequence[str]] = None,
|
107
|
-
labels: Optional[List[str]] = None,
|
108
108
|
use_after_commit: bool = False,
|
109
109
|
incremental: bool = False,
|
110
110
|
history_step: Optional[int] = None,
|
111
111
|
base_id: Optional[str] = None,
|
112
112
|
) -> Optional[Dict]:
|
113
|
-
aliases = aliases or []
|
114
113
|
alias_specs = []
|
115
|
-
for alias in aliases:
|
116
|
-
|
117
|
-
# Users can explicitly alias this artifact to names
|
118
|
-
# other than the primary one passed in by using the
|
119
|
-
# 'secondaryName:alias' notation.
|
120
|
-
idx = alias.index(":")
|
121
|
-
artifact_collection_name = alias[: idx - 1]
|
122
|
-
tag = alias[idx + 1 :]
|
123
|
-
else:
|
124
|
-
artifact_collection_name = name
|
125
|
-
tag = alias
|
126
|
-
alias_specs.append(
|
127
|
-
{
|
128
|
-
"artifactCollectionName": artifact_collection_name,
|
129
|
-
"alias": tag,
|
130
|
-
}
|
131
|
-
)
|
114
|
+
for alias in aliases or []:
|
115
|
+
alias_specs.append({"artifactCollectionName": name, "alias": alias})
|
132
116
|
|
133
117
|
"""Returns the server artifact."""
|
134
118
|
self._server_artifact, latest = self._api.create_artifact(
|
@@ -136,35 +120,27 @@ class ArtifactSaver:
|
|
136
120
|
name,
|
137
121
|
self._digest,
|
138
122
|
metadata=metadata,
|
123
|
+
ttl_duration_seconds=ttl_duration_seconds,
|
139
124
|
aliases=alias_specs,
|
140
|
-
labels=labels,
|
141
125
|
description=description,
|
142
126
|
is_user_created=self._is_user_created,
|
143
127
|
distributed_id=distributed_id,
|
144
128
|
client_id=client_id,
|
145
129
|
sequence_client_id=sequence_client_id,
|
146
|
-
enable_digest_deduplication=use_after_commit, # Reuse logical duplicates in the `use_artifact` flow
|
147
130
|
history_step=history_step,
|
148
131
|
)
|
149
132
|
|
150
|
-
# TODO(artifacts):
|
151
|
-
# if it's committed, all is good. If it's committing, just moving ahead isn't necessarily
|
152
|
-
# correct. It may be better to poll until it's committed or failed, and then decided what to
|
153
|
-
# do
|
154
133
|
assert self._server_artifact is not None # mypy optionality unwrapper
|
155
134
|
artifact_id = self._server_artifact["id"]
|
156
135
|
if base_id is None and latest:
|
157
136
|
base_id = latest["id"]
|
158
|
-
if
|
159
|
-
self._server_artifact["state"] == "COMMITTED"
|
160
|
-
or self._server_artifact["state"] == "COMMITTING"
|
161
|
-
):
|
162
|
-
# TODO: update aliases, labels, description etc?
|
137
|
+
if self._server_artifact["state"] == "COMMITTED":
|
163
138
|
if use_after_commit:
|
164
139
|
self._api.use_artifact(artifact_id)
|
165
140
|
return self._server_artifact
|
166
|
-
|
141
|
+
if (
|
167
142
|
self._server_artifact["state"] != "PENDING"
|
143
|
+
# For old servers, see https://github.com/wandb/wandb/pull/6190
|
168
144
|
and self._server_artifact["state"] != "DELETED"
|
169
145
|
):
|
170
146
|
raise Exception(
|
@@ -306,16 +282,3 @@ class ArtifactSaver:
|
|
306
282
|
os.remove(entry.local_path)
|
307
283
|
except OSError:
|
308
284
|
pass
|
309
|
-
|
310
|
-
|
311
|
-
def get_staging_dir() -> FilePathStr:
|
312
|
-
path = os.path.join(env.get_data_dir(), "artifacts", "staging")
|
313
|
-
try:
|
314
|
-
mkdir_exists_ok(path)
|
315
|
-
except OSError as e:
|
316
|
-
raise PermissionError(
|
317
|
-
f"Unable to write staging files to {path}. To fix this problem, please set "
|
318
|
-
f"{env.DATA_DIR} to a directory where you have the necessary write access."
|
319
|
-
) from e
|
320
|
-
|
321
|
-
return FilePathStr(os.path.abspath(os.path.expanduser(path)))
|
@@ -1,23 +1,23 @@
|
|
1
1
|
"""Artifact cache."""
|
2
2
|
import contextlib
|
3
|
+
import errno
|
3
4
|
import hashlib
|
4
5
|
import os
|
5
|
-
import
|
6
|
-
from
|
6
|
+
import shutil
|
7
|
+
from pathlib import Path
|
8
|
+
from tempfile import NamedTemporaryFile
|
9
|
+
from typing import IO, TYPE_CHECKING, ContextManager, Generator, Optional, Tuple
|
7
10
|
|
8
11
|
import wandb
|
9
12
|
from wandb import env, util
|
10
|
-
from wandb.
|
11
|
-
from wandb.sdk.lib.
|
12
|
-
from wandb.sdk.lib.filesystem import mkdir_exists_ok
|
13
|
+
from wandb.errors import term
|
14
|
+
from wandb.sdk.lib.filesystem import files_in
|
13
15
|
from wandb.sdk.lib.hashutil import B64MD5, ETag, b64_to_hex_id
|
14
16
|
from wandb.sdk.lib.paths import FilePathStr, StrPath, URIStr
|
15
17
|
|
16
18
|
if TYPE_CHECKING:
|
17
19
|
import sys
|
18
20
|
|
19
|
-
from wandb.sdk.artifacts.artifact import Artifact
|
20
|
-
|
21
21
|
if sys.version_info >= (3, 8):
|
22
22
|
from typing import Protocol
|
23
23
|
else:
|
@@ -29,26 +29,18 @@ if TYPE_CHECKING:
|
|
29
29
|
|
30
30
|
|
31
31
|
class ArtifactsCache:
|
32
|
-
_TMP_PREFIX = "tmp"
|
33
|
-
|
34
32
|
def __init__(self, cache_dir: StrPath) -> None:
|
35
|
-
self._cache_dir = cache_dir
|
36
|
-
|
37
|
-
self.
|
38
|
-
self.
|
39
|
-
self._artifacts_by_id: Dict[str, Artifact] = CappedDict()
|
40
|
-
self._artifacts_by_client_id: Dict[str, Artifact] = CappedDict()
|
33
|
+
self._cache_dir = Path(cache_dir)
|
34
|
+
self._obj_dir = self._cache_dir / "obj"
|
35
|
+
self._temp_dir = self._cache_dir / "tmp"
|
36
|
+
self._temp_dir.mkdir(parents=True, exist_ok=True)
|
41
37
|
|
42
38
|
def check_md5_obj_path(
|
43
39
|
self, b64_md5: B64MD5, size: int
|
44
40
|
) -> Tuple[FilePathStr, bool, "Opener"]:
|
45
41
|
hex_md5 = b64_to_hex_id(b64_md5)
|
46
|
-
path =
|
47
|
-
|
48
|
-
if os.path.isfile(path) and os.path.getsize(path) == size:
|
49
|
-
return FilePathStr(path), True, opener
|
50
|
-
mkdir_exists_ok(os.path.dirname(path))
|
51
|
-
return FilePathStr(path), False, opener
|
42
|
+
path = self._obj_dir / "md5" / hex_md5[:2] / hex_md5[2:]
|
43
|
+
return self._check_or_create(path, size)
|
52
44
|
|
53
45
|
# TODO(spencerpearson): this method at least needs its signature changed.
|
54
46
|
# An ETag is not (necessarily) a checksum.
|
@@ -62,103 +54,138 @@ class ArtifactsCache:
|
|
62
54
|
hashlib.sha256(url.encode("utf-8")).digest()
|
63
55
|
+ hashlib.sha256(etag.encode("utf-8")).digest()
|
64
56
|
).hexdigest()
|
65
|
-
path =
|
66
|
-
|
67
|
-
if os.path.isfile(path) and os.path.getsize(path) == size:
|
68
|
-
return FilePathStr(path), True, opener
|
69
|
-
mkdir_exists_ok(os.path.dirname(path))
|
70
|
-
return FilePathStr(path), False, opener
|
71
|
-
|
72
|
-
def get_artifact(self, artifact_id: str) -> Optional["Artifact"]:
|
73
|
-
return self._artifacts_by_id.get(artifact_id)
|
57
|
+
path = self._obj_dir / "etag" / hexhash[:2] / hexhash[2:]
|
58
|
+
return self._check_or_create(path, size)
|
74
59
|
|
75
|
-
def
|
76
|
-
|
77
|
-
|
78
|
-
self.
|
79
|
-
|
80
|
-
|
81
|
-
return self._artifacts_by_client_id.get(client_id)
|
60
|
+
def _check_or_create(
|
61
|
+
self, path: Path, size: int
|
62
|
+
) -> Tuple[FilePathStr, bool, "Opener"]:
|
63
|
+
opener = self._cache_opener(path, size)
|
64
|
+
hit = path.is_file() and path.stat().st_size == size
|
65
|
+
return FilePathStr(str(path)), hit, opener
|
82
66
|
|
83
|
-
def
|
84
|
-
self
|
67
|
+
def cleanup(
|
68
|
+
self,
|
69
|
+
target_size: Optional[int] = None,
|
70
|
+
target_fraction: Optional[float] = None,
|
71
|
+
remove_temp: bool = False,
|
72
|
+
) -> int:
|
73
|
+
"""Clean up the cache, removing the least recently used files first.
|
74
|
+
|
75
|
+
Args:
|
76
|
+
target_size: The target size of the cache in bytes. If the cache is larger
|
77
|
+
than this, we will remove the least recently used files until the cache
|
78
|
+
is smaller than this size.
|
79
|
+
target_fraction: The target fraction of the cache to reclaim. If the cache
|
80
|
+
is larger than this, we will remove the least recently used files until
|
81
|
+
the cache is smaller than this fraction of its current size. It is an
|
82
|
+
error to specify both target_size and target_fraction.
|
83
|
+
remove_temp: Whether to remove temporary files. Temporary files are files
|
84
|
+
that are currently being written to the cache. If remove_temp is True,
|
85
|
+
all temp files will be removed, regardless of the target_size or
|
86
|
+
target_fraction.
|
87
|
+
|
88
|
+
Returns:
|
89
|
+
The number of bytes reclaimed.
|
90
|
+
"""
|
91
|
+
if target_size is None and target_fraction is None:
|
92
|
+
# Default to clearing the entire cache.
|
93
|
+
target_size = 0
|
94
|
+
if target_size is not None and target_fraction is not None:
|
95
|
+
raise ValueError("Cannot specify both target_size and target_fraction")
|
96
|
+
if target_size and target_size < 0:
|
97
|
+
raise ValueError("target_size must be non-negative")
|
98
|
+
if target_fraction and (target_fraction < 0 or target_fraction > 1):
|
99
|
+
raise ValueError("target_fraction must be between 0 and 1")
|
85
100
|
|
86
|
-
def cleanup(self, target_size: int, remove_temp: bool = False) -> int:
|
87
101
|
bytes_reclaimed = 0
|
88
|
-
paths = {}
|
89
102
|
total_size = 0
|
90
103
|
temp_size = 0
|
91
|
-
|
92
|
-
|
104
|
+
|
105
|
+
# Remove all temporary files if requested. Otherwise sum their size.
|
106
|
+
for entry in files_in(self._temp_dir):
|
107
|
+
size = entry.stat().st_size
|
108
|
+
total_size += size
|
109
|
+
if remove_temp:
|
93
110
|
try:
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
if file.startswith(ArtifactsCache._TMP_PREFIX):
|
98
|
-
if remove_temp:
|
99
|
-
os.remove(path)
|
100
|
-
bytes_reclaimed += stat.st_size
|
101
|
-
else:
|
102
|
-
temp_size += stat.st_size
|
103
|
-
continue
|
111
|
+
os.remove(entry.path)
|
112
|
+
bytes_reclaimed += size
|
104
113
|
except OSError:
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
114
|
+
pass
|
115
|
+
else:
|
116
|
+
temp_size += size
|
109
117
|
if temp_size:
|
110
118
|
wandb.termwarn(
|
111
119
|
f"Cache contains {util.to_human_size(temp_size)} of temporary files. "
|
112
120
|
"Run `wandb artifact cleanup --remove-temp` to remove them."
|
113
121
|
)
|
114
122
|
|
115
|
-
|
116
|
-
for
|
117
|
-
|
118
|
-
|
123
|
+
entries = []
|
124
|
+
for file_entry in files_in(self._obj_dir):
|
125
|
+
total_size += file_entry.stat().st_size
|
126
|
+
entries.append(file_entry)
|
127
|
+
|
128
|
+
if target_fraction is not None:
|
129
|
+
target_size = int(total_size * target_fraction)
|
130
|
+
assert target_size is not None
|
119
131
|
|
132
|
+
for entry in sorted(entries, key=lambda x: x.stat().st_atime):
|
133
|
+
if total_size <= target_size:
|
134
|
+
return bytes_reclaimed
|
120
135
|
try:
|
121
|
-
os.remove(path)
|
136
|
+
os.remove(entry.path)
|
122
137
|
except OSError:
|
123
138
|
pass
|
139
|
+
total_size -= entry.stat().st_size
|
140
|
+
bytes_reclaimed += entry.stat().st_size
|
141
|
+
|
142
|
+
if total_size > target_size:
|
143
|
+
wandb.termerror(
|
144
|
+
f"Failed to reclaim enough space in {self._cache_dir}. Try running"
|
145
|
+
" `wandb artifact cleanup --remove-temp` to remove temporary files."
|
146
|
+
)
|
124
147
|
|
125
|
-
total_size -= stat.st_size
|
126
|
-
bytes_reclaimed += stat.st_size
|
127
148
|
return bytes_reclaimed
|
128
149
|
|
129
|
-
def
|
150
|
+
def _free_space(self) -> int:
|
151
|
+
"""Return the number of bytes of free space in the cache directory."""
|
152
|
+
return shutil.disk_usage(self._cache_dir)[2]
|
153
|
+
|
154
|
+
def _reserve_space(self, size: int) -> None:
|
155
|
+
"""If a `size` write would exceed disk space, remove cached items to make space.
|
156
|
+
|
157
|
+
Raises:
|
158
|
+
OSError: If there is not enough space to write `size` bytes, even after
|
159
|
+
removing cached items.
|
160
|
+
"""
|
161
|
+
if size <= self._free_space():
|
162
|
+
return
|
163
|
+
|
164
|
+
term.termwarn("Cache size exceeded. Attempting to reclaim space...")
|
165
|
+
self.cleanup(target_fraction=0.5)
|
166
|
+
if size <= self._free_space():
|
167
|
+
return
|
168
|
+
|
169
|
+
self.cleanup(target_size=0)
|
170
|
+
if size > self._free_space():
|
171
|
+
raise OSError(errno.ENOSPC, f"Insufficient free space in {self._cache_dir}")
|
172
|
+
|
173
|
+
def _cache_opener(self, path: Path, size: int) -> "Opener":
|
130
174
|
@contextlib.contextmanager
|
131
175
|
def helper(mode: str = "w") -> Generator[IO, None, None]:
|
132
176
|
if "a" in mode:
|
133
177
|
raise ValueError("Appending to cache files is not supported")
|
134
178
|
|
135
|
-
|
136
|
-
|
137
|
-
dirname, f"{ArtifactsCache._TMP_PREFIX}_{secrets.token_hex(8)}"
|
138
|
-
)
|
139
|
-
with util.fsync_open(tmp_file, mode=mode) as f:
|
140
|
-
yield f
|
141
|
-
|
179
|
+
self._reserve_space(size)
|
180
|
+
temp_file = NamedTemporaryFile(dir=self._temp_dir, mode=mode, delete=False)
|
142
181
|
try:
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
# writer firsts stages its writes to a temporary file in the cache.
|
151
|
-
# Once it is finished, we issue an atomic replace operation to update
|
152
|
-
# the cache. Although this can result in redundant downloads, this
|
153
|
-
# guarantees that readers can NEVER read incomplete files from the
|
154
|
-
# cache.
|
155
|
-
#
|
156
|
-
# IMPORTANT: Replace is NOT atomic across different filesystems. This why
|
157
|
-
# it is critical that the temporary files sit directly in the cache --
|
158
|
-
# they need to be on the same filesystem!
|
159
|
-
os.replace(tmp_file, path)
|
160
|
-
except AttributeError:
|
161
|
-
os.rename(tmp_file, path)
|
182
|
+
yield temp_file
|
183
|
+
temp_file.close()
|
184
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
185
|
+
os.replace(temp_file.name, path)
|
186
|
+
except Exception:
|
187
|
+
os.remove(temp_file.name)
|
188
|
+
raise
|
162
189
|
|
163
190
|
return helper
|
164
191
|
|
@@ -169,6 +196,5 @@ _artifacts_cache = None
|
|
169
196
|
def get_artifacts_cache() -> ArtifactsCache:
|
170
197
|
global _artifacts_cache
|
171
198
|
if _artifacts_cache is None:
|
172
|
-
|
173
|
-
_artifacts_cache = ArtifactsCache(cache_dir)
|
199
|
+
_artifacts_cache = ArtifactsCache(env.get_cache_dir() / "artifacts")
|
174
200
|
return _artifacts_cache
|
@@ -0,0 +1,25 @@
|
|
1
|
+
"""Manages artifact file staging.
|
2
|
+
|
3
|
+
Artifact files are copied to the staging area as soon as they are added to an artifact
|
4
|
+
in order to avoid file changes corrupting the artifact. Once the upload is complete, the
|
5
|
+
file should be moved to the artifact cache.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import os
|
9
|
+
|
10
|
+
from wandb import env
|
11
|
+
from wandb.sdk.lib.filesystem import mkdir_exists_ok
|
12
|
+
from wandb.sdk.lib.paths import FilePathStr
|
13
|
+
|
14
|
+
|
15
|
+
def get_staging_dir() -> FilePathStr:
|
16
|
+
path = os.path.join(env.get_data_dir(), "artifacts", "staging")
|
17
|
+
try:
|
18
|
+
mkdir_exists_ok(path)
|
19
|
+
except OSError as e:
|
20
|
+
raise PermissionError(
|
21
|
+
f"Unable to write staging files to {path}. To fix this problem, please set "
|
22
|
+
f"{env.DATA_DIR} to a directory where you have the necessary write access."
|
23
|
+
) from e
|
24
|
+
|
25
|
+
return FilePathStr(os.path.abspath(os.path.expanduser(path)))
|
@@ -204,7 +204,9 @@ class S3Handler(StorageHandler):
|
|
204
204
|
)
|
205
205
|
return entries
|
206
206
|
|
207
|
-
def _size_from_obj(
|
207
|
+
def _size_from_obj(
|
208
|
+
self, obj: Union["boto3.s3.Object", "boto3.s3.ObjectSummary"]
|
209
|
+
) -> int:
|
208
210
|
# ObjectSummary has size, Object has content_length
|
209
211
|
size: int
|
210
212
|
if hasattr(obj, "size"):
|
@@ -215,7 +217,7 @@ class S3Handler(StorageHandler):
|
|
215
217
|
|
216
218
|
def _entry_from_obj(
|
217
219
|
self,
|
218
|
-
obj: "boto3.s3.Object",
|
220
|
+
obj: Union["boto3.s3.Object", "boto3.s3.ObjectSummary"],
|
219
221
|
path: str,
|
220
222
|
name: Optional[StrPath] = None,
|
221
223
|
prefix: str = "",
|
@@ -261,18 +263,21 @@ class S3Handler(StorageHandler):
|
|
261
263
|
)
|
262
264
|
|
263
265
|
@staticmethod
|
264
|
-
def _etag_from_obj(obj: "boto3.s3.Object") -> ETag:
|
266
|
+
def _etag_from_obj(obj: Union["boto3.s3.Object", "boto3.s3.ObjectSummary"]) -> ETag:
|
265
267
|
etag: ETag
|
266
268
|
etag = obj.e_tag[1:-1] # escape leading and trailing quote
|
267
269
|
return etag
|
268
270
|
|
269
|
-
|
270
|
-
|
271
|
+
def _extra_from_obj(
|
272
|
+
self, obj: Union["boto3.s3.Object", "boto3.s3.ObjectSummary"]
|
273
|
+
) -> Dict[str, str]:
|
271
274
|
extra = {
|
272
275
|
"etag": obj.e_tag[1:-1], # escape leading and trailing quote
|
273
276
|
}
|
274
|
-
|
275
|
-
|
277
|
+
if not hasattr(obj, "version_id"):
|
278
|
+
# Convert ObjectSummary to Object to get the version_id.
|
279
|
+
obj = self._s3.Object(obj.bucket_name, obj.key) # type: ignore[union-attr]
|
280
|
+
if hasattr(obj, "version_id") and obj.version_id and obj.version_id != "null":
|
276
281
|
extra["versionID"] = obj.version_id
|
277
282
|
return extra
|
278
283
|
|
@@ -4,8 +4,8 @@ from typing import TYPE_CHECKING, Optional, Sequence, Union
|
|
4
4
|
|
5
5
|
import wandb
|
6
6
|
from wandb import util
|
7
|
+
from wandb.sdk.artifacts.artifact_cache import artifact_cache
|
7
8
|
from wandb.sdk.artifacts.artifact_manifest_entry import ArtifactManifestEntry
|
8
|
-
from wandb.sdk.artifacts.artifacts_cache import get_artifacts_cache
|
9
9
|
from wandb.sdk.artifacts.storage_handler import StorageHandler
|
10
10
|
from wandb.sdk.lib.paths import FilePathStr, StrPath, URIStr
|
11
11
|
|
@@ -20,7 +20,6 @@ class WBLocalArtifactHandler(StorageHandler):
|
|
20
20
|
|
21
21
|
def __init__(self) -> None:
|
22
22
|
self._scheme = "wandb-client-artifact"
|
23
|
-
self._cache = get_artifacts_cache()
|
24
23
|
|
25
24
|
def can_handle(self, parsed_url: "ParseResult") -> bool:
|
26
25
|
return parsed_url.scheme == self._scheme
|
@@ -54,7 +53,7 @@ class WBLocalArtifactHandler(StorageHandler):
|
|
54
53
|
"""
|
55
54
|
client_id = util.host_from_path(path)
|
56
55
|
target_path = util.uri_from_path(path)
|
57
|
-
target_artifact =
|
56
|
+
target_artifact = artifact_cache.get(client_id)
|
58
57
|
if not isinstance(target_artifact, wandb.Artifact):
|
59
58
|
raise RuntimeError("Local Artifact not found - invalid reference")
|
60
59
|
target_entry = target_artifact._manifest.entries[target_path]
|