aipmodel 0.2.65__tar.gz → 0.2.67__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {aipmodel-0.2.65 → aipmodel-0.2.67}/PKG-INFO +1 -1
- {aipmodel-0.2.65 → aipmodel-0.2.67}/README.md +50 -0
- {aipmodel-0.2.65 → aipmodel-0.2.67}/aipmodel/__init__.py +1 -1
- {aipmodel-0.2.65 → aipmodel-0.2.67}/aipmodel/model_registry.py +86 -0
- {aipmodel-0.2.65 → aipmodel-0.2.67}/aipmodel.egg-info/PKG-INFO +1 -1
- {aipmodel-0.2.65 → aipmodel-0.2.67}/setup.py +1 -1
- {aipmodel-0.2.65 → aipmodel-0.2.67}/MANIFEST.in +0 -0
- {aipmodel-0.2.65 → aipmodel-0.2.67}/aipmodel/CephS3Manager.py +0 -0
- {aipmodel-0.2.65 → aipmodel-0.2.67}/aipmodel/acl_manager.py +0 -0
- {aipmodel-0.2.65 → aipmodel-0.2.67}/aipmodel/template.py +0 -0
- {aipmodel-0.2.65 → aipmodel-0.2.67}/aipmodel/update_checker.py +0 -0
- {aipmodel-0.2.65 → aipmodel-0.2.67}/aipmodel.egg-info/SOURCES.txt +0 -0
- {aipmodel-0.2.65 → aipmodel-0.2.67}/aipmodel.egg-info/dependency_links.txt +0 -0
- {aipmodel-0.2.65 → aipmodel-0.2.67}/aipmodel.egg-info/requires.txt +0 -0
- {aipmodel-0.2.65 → aipmodel-0.2.67}/aipmodel.egg-info/top_level.txt +0 -0
- {aipmodel-0.2.65 → aipmodel-0.2.67}/requirements.txt +0 -0
- {aipmodel-0.2.65 → aipmodel-0.2.67}/setup.cfg +0 -0
|
@@ -51,6 +51,12 @@ S3_MAX_CONCURRENCY="10"
|
|
|
51
51
|
S3_UPLOAD_WORKERS="4"
|
|
52
52
|
S3_DOWNLOAD_WORKERS="4"
|
|
53
53
|
|
|
54
|
+
# Auto-retry for transient connection drops during large transfers
|
|
55
|
+
S3_MAX_RETRIES="5" # per-file retries before giving up
|
|
56
|
+
S3_RETRY_BACKOFF_SECONDS="2" # base backoff, grows exponentially per attempt
|
|
57
|
+
S3_RETRY_MAX_DELAY_SECONDS="30" # cap on a single backoff sleep
|
|
58
|
+
S3_BOTO_MAX_ATTEMPTS="10" # botocore request-level retry attempts (adaptive)
|
|
59
|
+
|
|
54
60
|
```
|
|
55
61
|
|
|
56
62
|
> **Note:** `CLEARML_WEB_HOST` is new. If not provided, the SDK will default to the API host, which might result in incorrect clickable links in the output.
|
|
@@ -68,6 +74,50 @@ S3_DOWNLOAD_WORKERS="4"
|
|
|
68
74
|
|
|
69
75
|
Nothing is ever logged to a file or hidden — all output goes to stdout via `print()`, so it's visible wherever the process runs.
|
|
70
76
|
|
|
77
|
+
## Resilient Transfers (Auto-Retry)
|
|
78
|
+
|
|
79
|
+
Large multi-GB uploads/downloads over Ceph/RGW occasionally hit a **transient**
|
|
80
|
+
connection drop mid-stream (`IncompleteRead` / `ProtocolError` / connection
|
|
81
|
+
reset). The SDK now **retries these automatically**, so `add_model`,
|
|
82
|
+
`get_model` and `download` complete without manual intervention:
|
|
83
|
+
|
|
84
|
+
- Each **individual file** transfer is retried on a transient error with
|
|
85
|
+
exponential backoff (`S3_RETRY_BACKOFF_SECONDS`, capped by
|
|
86
|
+
`S3_RETRY_MAX_DELAY_SECONDS`), up to `S3_MAX_RETRIES` times. Downloads reset
|
|
87
|
+
the partial file before each retry so no corrupt bytes remain.
|
|
88
|
+
- The underlying boto3 client also uses botocore's **adaptive** request-level
|
|
89
|
+
retries (`S3_BOTO_MAX_ATTEMPTS`).
|
|
90
|
+
- **Non-transient** errors (bad input, auth, not-found) are **not** retried —
|
|
91
|
+
they fail immediately as before.
|
|
92
|
+
- Fully backward compatible: method signatures, return values and the final
|
|
93
|
+
`ValueError` raised on ultimate failure are unchanged. All knobs are
|
|
94
|
+
`.env`-overridable with sane defaults.
|
|
95
|
+
|
|
96
|
+
## Concurrent Adds
|
|
97
|
+
|
|
98
|
+
`add_model` is **concurrency-safe** — you can run many adds at the same time:
|
|
99
|
+
|
|
100
|
+
- Adds with **different model names** run **fully in parallel** (own ClearML id,
|
|
101
|
+
own S3 prefix — no interference).
|
|
102
|
+
- Adds with the **same `(project, model_name)`** are **serialized** by a
|
|
103
|
+
process-wide lock. Otherwise two simultaneous same-name adds would race on the
|
|
104
|
+
existence check + auto version-number computation and could create a
|
|
105
|
+
**duplicate model** or **overwrite a version**. With the lock the second add
|
|
106
|
+
waits for the first, then correctly continues to the next version
|
|
107
|
+
(`_v2` → `_v3`, …) — no duplicates, no lost versions.
|
|
108
|
+
- Uploads are **staged**: files go to a temp S3 path first and are only moved to
|
|
109
|
+
the final version path once the whole upload succeeds; any failure rolls back
|
|
110
|
+
(deletes the new model if it was new + the temp folder) and re-raises, so a
|
|
111
|
+
crashed/aborted add never leaves a half-written version.
|
|
112
|
+
- **Duplicate `(model_name, version)` is rejected**: re-adding an existing
|
|
113
|
+
explicit version raises `ValueError` instead of silently overwriting it (omit
|
|
114
|
+
`version` to auto-generate the next unique one).
|
|
115
|
+
- **Orphaned temp folders** left behind by a hard-killed upload are swept
|
|
116
|
+
automatically on the next add to that model.
|
|
117
|
+
- The lock is **per-process**. One server process (e.g. a single `uvicorn`
|
|
118
|
+
worker) is fully protected; for multiple workers/replicas add a distributed
|
|
119
|
+
lock (ClearML/Redis).
|
|
120
|
+
|
|
71
121
|
## Access Control (ACL)
|
|
72
122
|
|
|
73
123
|
ACL is **off by default** and fully optional. Toggle it with one env var:
|
|
@@ -10,6 +10,7 @@ from huggingface_hub import snapshot_download
|
|
|
10
10
|
from typing import Optional, List, Dict, Any
|
|
11
11
|
import json
|
|
12
12
|
import logging
|
|
13
|
+
import threading
|
|
13
14
|
import time
|
|
14
15
|
import re
|
|
15
16
|
from tqdm import tqdm
|
|
@@ -22,6 +23,25 @@ load_dotenv()
|
|
|
22
23
|
|
|
23
24
|
logger = logging.getLogger(__name__)
|
|
24
25
|
|
|
26
|
+
# Process-wide locks that serialize concurrent add_model calls targeting the SAME
|
|
27
|
+
# (project, model_name). Two simultaneous adds of the same name would otherwise
|
|
28
|
+
# race on the existence check + version-number computation and could create a
|
|
29
|
+
# duplicate model or clobber a version. Different names lock on different keys and
|
|
30
|
+
# still run fully in parallel. NOTE: per-process — with multiple API workers /
|
|
31
|
+
# replicas a distributed lock would be required for the same guarantee.
|
|
32
|
+
_ADD_MODEL_LOCKS = {}
|
|
33
|
+
_ADD_MODEL_LOCKS_GUARD = threading.Lock()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _get_add_model_lock(project_id, model_name):
|
|
37
|
+
key = (project_id, model_name)
|
|
38
|
+
with _ADD_MODEL_LOCKS_GUARD:
|
|
39
|
+
lock = _ADD_MODEL_LOCKS.get(key)
|
|
40
|
+
if lock is None:
|
|
41
|
+
lock = threading.Lock()
|
|
42
|
+
_ADD_MODEL_LOCKS[key] = lock
|
|
43
|
+
return lock
|
|
44
|
+
|
|
25
45
|
class ProjectsAPI:
|
|
26
46
|
def __init__(self, post, verbose):
|
|
27
47
|
self.verbose = verbose
|
|
@@ -667,9 +687,59 @@ class MLOpsManager:
|
|
|
667
687
|
if os.path.exists(tmp_dir):
|
|
668
688
|
shutil.rmtree(tmp_dir)
|
|
669
689
|
|
|
690
|
+
def _cleanup_orphan_temp_folders(self, manager, model_id, keep_prefix=None):
|
|
691
|
+
"""
|
|
692
|
+
Best-effort removal of leftover `*_tmp_*` staging folders under a model —
|
|
693
|
+
orphans from a previously interrupted/crashed upload (where rollback
|
|
694
|
+
never ran). Safe because add_model holds a per-(project, model_name)
|
|
695
|
+
lock, so no other add for this model is running concurrently. Never
|
|
696
|
+
raises: cleanup must never break an otherwise-valid add.
|
|
697
|
+
"""
|
|
698
|
+
try:
|
|
699
|
+
base = f"_models/{model_id}/"
|
|
700
|
+
keys = manager.list_folder_contents(base)
|
|
701
|
+
temp_prefixes = set()
|
|
702
|
+
for key in keys:
|
|
703
|
+
rel = key[len(base):] if key.startswith(base) else key
|
|
704
|
+
top = rel.split("/", 1)[0]
|
|
705
|
+
prefix = f"{base}{top}/"
|
|
706
|
+
if "_tmp_" in top and prefix != keep_prefix:
|
|
707
|
+
temp_prefixes.add(prefix)
|
|
708
|
+
for prefix in temp_prefixes:
|
|
709
|
+
try:
|
|
710
|
+
manager.delete_folder(prefix)
|
|
711
|
+
if self.verbose:
|
|
712
|
+
print(f"[SDK_INFO] Cleaned orphan temp folder: {prefix}")
|
|
713
|
+
except Exception:
|
|
714
|
+
pass
|
|
715
|
+
except Exception:
|
|
716
|
+
pass
|
|
717
|
+
|
|
670
718
|
def add_model(self, source_type, model_name=None, version=None, source_path=None, code_path=None, category=None, tags=None,
|
|
671
719
|
public=False, external_ceph_endpoint_url=None, external_ceph_bucket_name=None, external_ceph_access_key=None, external_ceph_secret_key=None,
|
|
672
720
|
model_folder_name=None):
|
|
721
|
+
"""
|
|
722
|
+
Register/add a model version. Concurrency-safe: adds that target the SAME
|
|
723
|
+
(project, model_name) are serialized by a process-wide lock so simultaneous
|
|
724
|
+
same-name adds cannot create a duplicate model or collide on the auto
|
|
725
|
+
version number; adds with different names run fully in parallel. All logic
|
|
726
|
+
lives in _add_model_impl — signature, behavior and return value are
|
|
727
|
+
unchanged (this wrapper only adds the lock).
|
|
728
|
+
"""
|
|
729
|
+
with _get_add_model_lock(self.project_id, model_name):
|
|
730
|
+
return self._add_model_impl(
|
|
731
|
+
source_type, model_name=model_name, version=version, source_path=source_path,
|
|
732
|
+
code_path=code_path, category=category, tags=tags, public=public,
|
|
733
|
+
external_ceph_endpoint_url=external_ceph_endpoint_url,
|
|
734
|
+
external_ceph_bucket_name=external_ceph_bucket_name,
|
|
735
|
+
external_ceph_access_key=external_ceph_access_key,
|
|
736
|
+
external_ceph_secret_key=external_ceph_secret_key,
|
|
737
|
+
model_folder_name=model_folder_name,
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
def _add_model_impl(self, source_type, model_name=None, version=None, source_path=None, code_path=None, category=None, tags=None,
|
|
741
|
+
public=False, external_ceph_endpoint_url=None, external_ceph_bucket_name=None, external_ceph_access_key=None, external_ceph_secret_key=None,
|
|
742
|
+
model_folder_name=None):
|
|
673
743
|
|
|
674
744
|
if self.verbose:
|
|
675
745
|
print(f"[SDK_INFO] Adding model: {model_name} (Source: {source_type}, Public: {public})")
|
|
@@ -775,6 +845,17 @@ class MLOpsManager:
|
|
|
775
845
|
max_version_number = vn
|
|
776
846
|
except Exception:
|
|
777
847
|
pass
|
|
848
|
+
|
|
849
|
+
# Reject a duplicate (model_name, version): if the caller passed an
|
|
850
|
+
# explicit version that already exists, fail instead of silently
|
|
851
|
+
# overwriting it. Omitting version (auto-generation) is always unique.
|
|
852
|
+
if version and version in versions_map:
|
|
853
|
+
error_message = (
|
|
854
|
+
f"Version '{version}' already exists for model '{model_name}'. "
|
|
855
|
+
f"Choose a different version name, or omit 'version' to auto-generate the next one."
|
|
856
|
+
)
|
|
857
|
+
print(f"[SDK_FAIL] {error_message}")
|
|
858
|
+
raise ValueError(error_message)
|
|
778
859
|
else:
|
|
779
860
|
if self.verbose:
|
|
780
861
|
print("[SDK_INFO] Creating new model entry...")
|
|
@@ -803,6 +884,11 @@ class MLOpsManager:
|
|
|
803
884
|
else:
|
|
804
885
|
model_folder_name = "unknown"
|
|
805
886
|
|
|
887
|
+
# Sweep orphaned temp folders from any previously interrupted upload
|
|
888
|
+
# (safe under the per-name lock). New models have none.
|
|
889
|
+
if not is_new_model:
|
|
890
|
+
self._cleanup_orphan_temp_folders(current_manager, model_id)
|
|
891
|
+
|
|
806
892
|
dest_version_path = f"_models/{model_id}/{version}/"
|
|
807
893
|
temp_suffix = self.generate_random_string()
|
|
808
894
|
dest_temp_path = f"_models/{model_id}/{version}_tmp_{temp_suffix}/"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|