podstack 1.2.0__py3-none-any.whl
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.
- podstack/__init__.py +222 -0
- podstack/annotations.py +725 -0
- podstack/client.py +322 -0
- podstack/exceptions.py +125 -0
- podstack/execution.py +291 -0
- podstack/gpu_runner.py +1141 -0
- podstack/models.py +274 -0
- podstack/notebook.py +410 -0
- podstack/registry/__init__.py +402 -0
- podstack/registry/client.py +957 -0
- podstack/registry/exceptions.py +107 -0
- podstack/registry/experiment.py +227 -0
- podstack/registry/model.py +273 -0
- podstack/registry/model_utils.py +231 -0
- podstack-1.2.0.dist-info/METADATA +299 -0
- podstack-1.2.0.dist-info/RECORD +27 -0
- podstack-1.2.0.dist-info/WHEEL +5 -0
- podstack-1.2.0.dist-info/licenses/LICENSE +21 -0
- podstack-1.2.0.dist-info/top_level.txt +2 -0
- podstack_gpu/__init__.py +126 -0
- podstack_gpu/app.py +675 -0
- podstack_gpu/exceptions.py +35 -0
- podstack_gpu/image.py +325 -0
- podstack_gpu/runner.py +746 -0
- podstack_gpu/secret.py +189 -0
- podstack_gpu/utils.py +203 -0
- podstack_gpu/volume.py +198 -0
podstack_gpu/secret.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Podstack Secret - Secure credential management for GPU functions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import os
|
|
5
|
+
from typing import Optional, Dict
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class SecretConfig:
|
|
11
|
+
"""Secret configuration."""
|
|
12
|
+
name: str
|
|
13
|
+
env_var: Optional[str] = None # Environment variable to inject as
|
|
14
|
+
|
|
15
|
+
def to_dict(self) -> dict:
|
|
16
|
+
return {
|
|
17
|
+
"name": self.name,
|
|
18
|
+
"env_var": self.env_var,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Secret:
|
|
23
|
+
"""
|
|
24
|
+
Secure secrets that can be injected into GPU functions.
|
|
25
|
+
|
|
26
|
+
Secrets are stored encrypted and only decrypted at runtime.
|
|
27
|
+
They can be injected as environment variables or accessed via the API.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
# Reference a secret stored in Podstack
|
|
31
|
+
hf_token = podstack.Secret.from_name("huggingface-token")
|
|
32
|
+
|
|
33
|
+
@app.function(gpu="H100", secrets=[hf_token])
|
|
34
|
+
def train():
|
|
35
|
+
import os
|
|
36
|
+
# Secret is available as environment variable
|
|
37
|
+
token = os.environ["HUGGINGFACE_TOKEN"]
|
|
38
|
+
|
|
39
|
+
# Or inject from environment at deploy time
|
|
40
|
+
api_key = podstack.Secret.from_local_env("OPENAI_API_KEY")
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, config: SecretConfig):
|
|
44
|
+
self._config = config
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_name(
|
|
48
|
+
cls,
|
|
49
|
+
name: str,
|
|
50
|
+
environment_variable: str = None,
|
|
51
|
+
required: bool = True,
|
|
52
|
+
) -> "Secret":
|
|
53
|
+
"""
|
|
54
|
+
Reference a secret stored in Podstack by name.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
name: Secret name in Podstack
|
|
58
|
+
environment_variable: Env var name to inject as (defaults to uppercased name)
|
|
59
|
+
required: If True, fail if secret doesn't exist
|
|
60
|
+
|
|
61
|
+
Example:
|
|
62
|
+
secret = podstack.Secret.from_name(
|
|
63
|
+
"huggingface-token",
|
|
64
|
+
environment_variable="HF_TOKEN"
|
|
65
|
+
)
|
|
66
|
+
"""
|
|
67
|
+
env_var = environment_variable or name.upper().replace("-", "_")
|
|
68
|
+
return cls(SecretConfig(name=name, env_var=env_var))
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def from_local_env(
|
|
72
|
+
cls,
|
|
73
|
+
env_var: str,
|
|
74
|
+
remote_name: str = None,
|
|
75
|
+
) -> "Secret":
|
|
76
|
+
"""
|
|
77
|
+
Create a secret from a local environment variable.
|
|
78
|
+
|
|
79
|
+
The value is read at deploy time and stored securely.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
env_var: Local environment variable name
|
|
83
|
+
remote_name: Name to use remotely (defaults to env_var)
|
|
84
|
+
|
|
85
|
+
Example:
|
|
86
|
+
# Read OPENAI_API_KEY from local env and inject into function
|
|
87
|
+
secret = podstack.Secret.from_local_env("OPENAI_API_KEY")
|
|
88
|
+
"""
|
|
89
|
+
return cls(SecretConfig(
|
|
90
|
+
name=remote_name or env_var,
|
|
91
|
+
env_var=env_var,
|
|
92
|
+
))
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def from_dict(cls, secrets: Dict[str, str]) -> "Secret":
|
|
96
|
+
"""
|
|
97
|
+
Create a secret from a dictionary (for testing/development).
|
|
98
|
+
|
|
99
|
+
WARNING: Do not use in production! Values will be in code.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
secrets: Dictionary of secret key-value pairs
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
# Only for local testing!
|
|
106
|
+
secret = podstack.Secret.from_dict({"API_KEY": "test-key"})
|
|
107
|
+
"""
|
|
108
|
+
# Create a composite secret
|
|
109
|
+
return cls(SecretConfig(name="_dict_secret"))
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def from_dotenv(cls, path: str = ".env") -> "Secret":
|
|
113
|
+
"""
|
|
114
|
+
Load secrets from a .env file.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
path: Path to .env file
|
|
118
|
+
|
|
119
|
+
Example:
|
|
120
|
+
secrets = podstack.Secret.from_dotenv(".env.production")
|
|
121
|
+
"""
|
|
122
|
+
return cls(SecretConfig(name=f"_dotenv:{path}"))
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def name(self) -> str:
|
|
126
|
+
"""Get the secret name."""
|
|
127
|
+
return self._config.name
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def env_var(self) -> str:
|
|
131
|
+
"""Get the environment variable name."""
|
|
132
|
+
return self._config.env_var
|
|
133
|
+
|
|
134
|
+
def to_dict(self) -> dict:
|
|
135
|
+
"""Convert to dictionary for API serialization."""
|
|
136
|
+
return self._config.to_dict()
|
|
137
|
+
|
|
138
|
+
def __repr__(self) -> str:
|
|
139
|
+
return f"Secret(name={self._config.name!r})"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class SecretDict:
|
|
143
|
+
"""
|
|
144
|
+
A dictionary of secrets that can be accessed in GPU functions.
|
|
145
|
+
|
|
146
|
+
Example:
|
|
147
|
+
secrets = podstack.SecretDict.from_name("my-secrets")
|
|
148
|
+
|
|
149
|
+
@app.function(gpu="H100", secrets=[secrets])
|
|
150
|
+
def train():
|
|
151
|
+
# Access secrets by key
|
|
152
|
+
api_key = secrets["API_KEY"]
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def __init__(self, name: str):
|
|
156
|
+
self._name = name
|
|
157
|
+
self._values: Dict[str, str] = {}
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def from_name(cls, name: str) -> "SecretDict":
|
|
161
|
+
"""
|
|
162
|
+
Reference a secret dictionary stored in Podstack.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
name: Secret dictionary name
|
|
166
|
+
"""
|
|
167
|
+
return cls(name)
|
|
168
|
+
|
|
169
|
+
def __getitem__(self, key: str) -> str:
|
|
170
|
+
"""Get a secret value by key."""
|
|
171
|
+
if key in self._values:
|
|
172
|
+
return self._values[key]
|
|
173
|
+
# In remote execution, this would be populated
|
|
174
|
+
return os.environ.get(f"{self._name.upper()}_{key.upper()}", "")
|
|
175
|
+
|
|
176
|
+
def get(self, key: str, default: str = None) -> Optional[str]:
|
|
177
|
+
"""Get a secret value with a default."""
|
|
178
|
+
try:
|
|
179
|
+
return self[key]
|
|
180
|
+
except KeyError:
|
|
181
|
+
return default
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def name(self) -> str:
|
|
185
|
+
"""Get the secret dict name."""
|
|
186
|
+
return self._name
|
|
187
|
+
|
|
188
|
+
def __repr__(self) -> str:
|
|
189
|
+
return f"SecretDict(name={self._name!r})"
|
podstack_gpu/utils.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Podstack GPU Utilities - Helper functions for GPU operations."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import os
|
|
5
|
+
from typing import Optional, Dict, Any, List
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def nvidia_smi(format: str = "text") -> str:
|
|
9
|
+
"""
|
|
10
|
+
Get nvidia-smi output.
|
|
11
|
+
|
|
12
|
+
This function runs nvidia-smi and returns the output.
|
|
13
|
+
Use this inside your GPU functions to check GPU status.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
format: Output format - "text" (default), "csv", or "xml"
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
nvidia-smi output as string
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
@app.function(gpu="L40S")
|
|
23
|
+
def check_gpu():
|
|
24
|
+
from podstack import nvidia_smi
|
|
25
|
+
print(nvidia_smi())
|
|
26
|
+
return {"gpu_info": nvidia_smi("csv")}
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
if format == "csv":
|
|
30
|
+
cmd = ["nvidia-smi", "--query-gpu=name,memory.total,memory.used,memory.free,utilization.gpu,temperature.gpu", "--format=csv"]
|
|
31
|
+
elif format == "xml":
|
|
32
|
+
cmd = ["nvidia-smi", "-x", "-q"]
|
|
33
|
+
else:
|
|
34
|
+
cmd = ["nvidia-smi"]
|
|
35
|
+
|
|
36
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
|
37
|
+
return result.stdout if result.returncode == 0 else result.stderr
|
|
38
|
+
except FileNotFoundError:
|
|
39
|
+
return "nvidia-smi not found. Make sure NVIDIA drivers are installed."
|
|
40
|
+
except subprocess.TimeoutExpired:
|
|
41
|
+
return "nvidia-smi timed out"
|
|
42
|
+
except Exception as e:
|
|
43
|
+
return f"Error running nvidia-smi: {e}"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def gpu_info() -> Dict[str, Any]:
|
|
47
|
+
"""
|
|
48
|
+
Get GPU information as a dictionary.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Dictionary with GPU information including:
|
|
52
|
+
- name: GPU model name
|
|
53
|
+
- memory_total: Total memory in MB
|
|
54
|
+
- memory_used: Used memory in MB
|
|
55
|
+
- memory_free: Free memory in MB
|
|
56
|
+
- utilization: GPU utilization percentage
|
|
57
|
+
- temperature: GPU temperature in Celsius
|
|
58
|
+
- count: Number of GPUs
|
|
59
|
+
|
|
60
|
+
Example:
|
|
61
|
+
@app.function(gpu="L40S")
|
|
62
|
+
def check_resources():
|
|
63
|
+
from podstack import gpu_info
|
|
64
|
+
info = gpu_info()
|
|
65
|
+
print(f"GPU: {info['name']}")
|
|
66
|
+
print(f"Memory: {info['memory_used']}/{info['memory_total']} MB")
|
|
67
|
+
return info
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
result = subprocess.run(
|
|
71
|
+
["nvidia-smi", "--query-gpu=name,memory.total,memory.used,memory.free,utilization.gpu,temperature.gpu,count", "--format=csv,noheader,nounits"],
|
|
72
|
+
capture_output=True, text=True, timeout=10
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if result.returncode != 0:
|
|
76
|
+
return {"error": result.stderr}
|
|
77
|
+
|
|
78
|
+
lines = result.stdout.strip().split("\n")
|
|
79
|
+
gpus = []
|
|
80
|
+
|
|
81
|
+
for line in lines:
|
|
82
|
+
parts = [p.strip() for p in line.split(",")]
|
|
83
|
+
if len(parts) >= 6:
|
|
84
|
+
gpus.append({
|
|
85
|
+
"name": parts[0],
|
|
86
|
+
"memory_total": int(parts[1]) if parts[1].isdigit() else parts[1],
|
|
87
|
+
"memory_used": int(parts[2]) if parts[2].isdigit() else parts[2],
|
|
88
|
+
"memory_free": int(parts[3]) if parts[3].isdigit() else parts[3],
|
|
89
|
+
"utilization": int(parts[4]) if parts[4].isdigit() else parts[4],
|
|
90
|
+
"temperature": int(parts[5]) if parts[5].isdigit() else parts[5],
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
if len(gpus) == 1:
|
|
94
|
+
return gpus[0]
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
"count": len(gpus),
|
|
98
|
+
"gpus": gpus,
|
|
99
|
+
"name": gpus[0]["name"] if gpus else "Unknown",
|
|
100
|
+
"memory_total": sum(g.get("memory_total", 0) for g in gpus if isinstance(g.get("memory_total"), int)),
|
|
101
|
+
"memory_used": sum(g.get("memory_used", 0) for g in gpus if isinstance(g.get("memory_used"), int)),
|
|
102
|
+
"memory_free": sum(g.get("memory_free", 0) for g in gpus if isinstance(g.get("memory_free"), int)),
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
except FileNotFoundError:
|
|
106
|
+
return {"error": "nvidia-smi not found"}
|
|
107
|
+
except Exception as e:
|
|
108
|
+
return {"error": str(e)}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def cuda_available() -> bool:
|
|
112
|
+
"""
|
|
113
|
+
Check if CUDA is available.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
True if CUDA is available, False otherwise
|
|
117
|
+
|
|
118
|
+
Example:
|
|
119
|
+
@app.function(gpu="L40S")
|
|
120
|
+
def train():
|
|
121
|
+
from podstack import cuda_available
|
|
122
|
+
if cuda_available():
|
|
123
|
+
print("CUDA is ready!")
|
|
124
|
+
else:
|
|
125
|
+
print("Warning: CUDA not available")
|
|
126
|
+
"""
|
|
127
|
+
try:
|
|
128
|
+
import torch
|
|
129
|
+
return torch.cuda.is_available()
|
|
130
|
+
except ImportError:
|
|
131
|
+
# Fallback to checking nvidia-smi
|
|
132
|
+
try:
|
|
133
|
+
result = subprocess.run(["nvidia-smi"], capture_output=True, timeout=5)
|
|
134
|
+
return result.returncode == 0
|
|
135
|
+
except:
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def cuda_device_count() -> int:
|
|
140
|
+
"""
|
|
141
|
+
Get the number of available CUDA devices.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Number of CUDA devices
|
|
145
|
+
|
|
146
|
+
Example:
|
|
147
|
+
@app.function(gpu="A100-80G", count=4)
|
|
148
|
+
def distributed_train():
|
|
149
|
+
from podstack import cuda_device_count
|
|
150
|
+
print(f"Training on {cuda_device_count()} GPUs")
|
|
151
|
+
"""
|
|
152
|
+
try:
|
|
153
|
+
import torch
|
|
154
|
+
return torch.cuda.device_count()
|
|
155
|
+
except ImportError:
|
|
156
|
+
# Fallback
|
|
157
|
+
info = gpu_info()
|
|
158
|
+
return info.get("count", 1) if "error" not in info else 0
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def print_gpu_status():
|
|
162
|
+
"""
|
|
163
|
+
Print a formatted GPU status summary.
|
|
164
|
+
|
|
165
|
+
Example:
|
|
166
|
+
@app.function(gpu="L40S")
|
|
167
|
+
def my_function():
|
|
168
|
+
from podstack import print_gpu_status
|
|
169
|
+
print_gpu_status()
|
|
170
|
+
# ... your code ...
|
|
171
|
+
"""
|
|
172
|
+
print("=" * 60)
|
|
173
|
+
print("GPU STATUS")
|
|
174
|
+
print("=" * 60)
|
|
175
|
+
|
|
176
|
+
info = gpu_info()
|
|
177
|
+
|
|
178
|
+
if "error" in info:
|
|
179
|
+
print(f"Error: {info['error']}")
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
if "gpus" in info:
|
|
183
|
+
# Multiple GPUs
|
|
184
|
+
print(f"Total GPUs: {info['count']}")
|
|
185
|
+
print(f"Total Memory: {info['memory_used']:,} / {info['memory_total']:,} MB")
|
|
186
|
+
print("-" * 60)
|
|
187
|
+
for i, gpu in enumerate(info["gpus"]):
|
|
188
|
+
print(f"GPU {i}: {gpu['name']}")
|
|
189
|
+
print(f" Memory: {gpu['memory_used']:,} / {gpu['memory_total']:,} MB ({gpu['memory_free']:,} MB free)")
|
|
190
|
+
print(f" Utilization: {gpu['utilization']}%")
|
|
191
|
+
print(f" Temperature: {gpu['temperature']}°C")
|
|
192
|
+
else:
|
|
193
|
+
# Single GPU
|
|
194
|
+
print(f"GPU: {info.get('name', 'Unknown')}")
|
|
195
|
+
mem_used = info.get('memory_used', 0)
|
|
196
|
+
mem_total = info.get('memory_total', 0)
|
|
197
|
+
mem_free = info.get('memory_free', 0)
|
|
198
|
+
if isinstance(mem_used, int) and isinstance(mem_total, int):
|
|
199
|
+
print(f"Memory: {mem_used:,} / {mem_total:,} MB ({mem_free:,} MB free)")
|
|
200
|
+
print(f"Utilization: {info.get('utilization', 'N/A')}%")
|
|
201
|
+
print(f"Temperature: {info.get('temperature', 'N/A')}°C")
|
|
202
|
+
|
|
203
|
+
print("=" * 60)
|
podstack_gpu/volume.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Podstack Volume - Persistent storage for GPU functions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import os
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class VolumeConfig:
|
|
11
|
+
"""Volume configuration."""
|
|
12
|
+
name: str
|
|
13
|
+
size_gb: int = 10
|
|
14
|
+
region: Optional[str] = None
|
|
15
|
+
|
|
16
|
+
def to_dict(self) -> dict:
|
|
17
|
+
return {
|
|
18
|
+
"name": self.name,
|
|
19
|
+
"size_gb": self.size_gb,
|
|
20
|
+
"region": self.region,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Volume:
|
|
25
|
+
"""
|
|
26
|
+
Persistent network storage that can be attached to GPU functions.
|
|
27
|
+
|
|
28
|
+
Volumes persist data across function invocations and can be shared
|
|
29
|
+
between functions.
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
# Create a volume
|
|
33
|
+
model_volume = podstack.Volume.from_name("model-cache", create_if_missing=True)
|
|
34
|
+
|
|
35
|
+
@app.function(gpu="H100", volumes={"/models": model_volume})
|
|
36
|
+
def train():
|
|
37
|
+
# Save models to /models - they persist!
|
|
38
|
+
torch.save(model, "/models/checkpoint.pt")
|
|
39
|
+
|
|
40
|
+
@app.function(gpu="H100", volumes={"/models": model_volume})
|
|
41
|
+
def inference():
|
|
42
|
+
# Load the model saved during training
|
|
43
|
+
model = torch.load("/models/checkpoint.pt")
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, config: VolumeConfig):
|
|
47
|
+
self._config = config
|
|
48
|
+
self._persisted = False
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def from_name(
|
|
52
|
+
cls,
|
|
53
|
+
name: str,
|
|
54
|
+
create_if_missing: bool = True,
|
|
55
|
+
size_gb: int = 10,
|
|
56
|
+
region: str = None,
|
|
57
|
+
) -> "Volume":
|
|
58
|
+
"""
|
|
59
|
+
Get or create a volume by name.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
name: Volume name (must be unique within project)
|
|
63
|
+
create_if_missing: Create the volume if it doesn't exist
|
|
64
|
+
size_gb: Size in GB (only used when creating)
|
|
65
|
+
region: Region for the volume (optional)
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
volume = podstack.Volume.from_name("my-data", size_gb=100)
|
|
69
|
+
"""
|
|
70
|
+
return cls(VolumeConfig(
|
|
71
|
+
name=name,
|
|
72
|
+
size_gb=size_gb,
|
|
73
|
+
region=region,
|
|
74
|
+
))
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def ephemeral(cls, size_gb: int = 10) -> "Volume":
|
|
78
|
+
"""
|
|
79
|
+
Create an ephemeral volume that only lasts for the function invocation.
|
|
80
|
+
|
|
81
|
+
Useful for temporary storage during execution.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
size_gb: Size in GB
|
|
85
|
+
|
|
86
|
+
Example:
|
|
87
|
+
temp = podstack.Volume.ephemeral(size_gb=50)
|
|
88
|
+
|
|
89
|
+
@app.function(gpu="H100", volumes={"/scratch": temp})
|
|
90
|
+
def process():
|
|
91
|
+
# Use /scratch for temporary files
|
|
92
|
+
...
|
|
93
|
+
"""
|
|
94
|
+
import uuid
|
|
95
|
+
return cls(VolumeConfig(
|
|
96
|
+
name=f"ephemeral-{uuid.uuid4().hex[:8]}",
|
|
97
|
+
size_gb=size_gb,
|
|
98
|
+
))
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def name(self) -> str:
|
|
102
|
+
"""Get the volume name."""
|
|
103
|
+
return self._config.name
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def size_gb(self) -> int:
|
|
107
|
+
"""Get the volume size in GB."""
|
|
108
|
+
return self._config.size_gb
|
|
109
|
+
|
|
110
|
+
def to_dict(self) -> dict:
|
|
111
|
+
"""Convert to dictionary for API serialization."""
|
|
112
|
+
return self._config.to_dict()
|
|
113
|
+
|
|
114
|
+
def __repr__(self) -> str:
|
|
115
|
+
return f"Volume(name={self._config.name!r}, size_gb={self._config.size_gb})"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class CloudBucketMount:
|
|
119
|
+
"""
|
|
120
|
+
Mount a cloud storage bucket (S3, GCS, etc.) as a volume.
|
|
121
|
+
|
|
122
|
+
Example:
|
|
123
|
+
bucket = podstack.CloudBucketMount.from_s3(
|
|
124
|
+
bucket_name="my-training-data",
|
|
125
|
+
secret=podstack.Secret.from_name("aws-credentials"),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
@app.function(gpu="H100", volumes={"/data": bucket})
|
|
129
|
+
def train():
|
|
130
|
+
# Access S3 data at /data
|
|
131
|
+
data = load_data("/data/dataset.parquet")
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
def __init__(
|
|
135
|
+
self,
|
|
136
|
+
bucket_name: str,
|
|
137
|
+
provider: str,
|
|
138
|
+
secret: "Secret" = None,
|
|
139
|
+
read_only: bool = False,
|
|
140
|
+
prefix: str = None,
|
|
141
|
+
):
|
|
142
|
+
self._bucket_name = bucket_name
|
|
143
|
+
self._provider = provider
|
|
144
|
+
self._secret = secret
|
|
145
|
+
self._read_only = read_only
|
|
146
|
+
self._prefix = prefix
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def from_s3(
|
|
150
|
+
cls,
|
|
151
|
+
bucket_name: str,
|
|
152
|
+
secret: "Secret" = None,
|
|
153
|
+
read_only: bool = False,
|
|
154
|
+
prefix: str = None,
|
|
155
|
+
) -> "CloudBucketMount":
|
|
156
|
+
"""
|
|
157
|
+
Mount an S3 bucket.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
bucket_name: S3 bucket name
|
|
161
|
+
secret: Secret containing AWS credentials
|
|
162
|
+
read_only: Mount as read-only
|
|
163
|
+
prefix: Only mount objects with this prefix
|
|
164
|
+
"""
|
|
165
|
+
return cls(bucket_name, "s3", secret, read_only, prefix)
|
|
166
|
+
|
|
167
|
+
@classmethod
|
|
168
|
+
def from_gcs(
|
|
169
|
+
cls,
|
|
170
|
+
bucket_name: str,
|
|
171
|
+
secret: "Secret" = None,
|
|
172
|
+
read_only: bool = False,
|
|
173
|
+
prefix: str = None,
|
|
174
|
+
) -> "CloudBucketMount":
|
|
175
|
+
"""
|
|
176
|
+
Mount a Google Cloud Storage bucket.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
bucket_name: GCS bucket name
|
|
180
|
+
secret: Secret containing GCP credentials
|
|
181
|
+
read_only: Mount as read-only
|
|
182
|
+
prefix: Only mount objects with this prefix
|
|
183
|
+
"""
|
|
184
|
+
return cls(bucket_name, "gcs", secret, read_only, prefix)
|
|
185
|
+
|
|
186
|
+
def to_dict(self) -> dict:
|
|
187
|
+
"""Convert to dictionary for API serialization."""
|
|
188
|
+
return {
|
|
189
|
+
"type": "cloud_bucket",
|
|
190
|
+
"bucket_name": self._bucket_name,
|
|
191
|
+
"provider": self._provider,
|
|
192
|
+
"secret": self._secret.name if self._secret else None,
|
|
193
|
+
"read_only": self._read_only,
|
|
194
|
+
"prefix": self._prefix,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
def __repr__(self) -> str:
|
|
198
|
+
return f"CloudBucketMount({self._provider}://{self._bucket_name})"
|