fal 1.2.1__py3-none-any.whl → 1.7.2__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.
Potentially problematic release.
This version of fal might be problematic. Click here for more details.
- fal/__main__.py +3 -1
- fal/_fal_version.py +2 -2
- fal/api.py +88 -20
- fal/app.py +221 -27
- fal/apps.py +147 -3
- fal/auth/__init__.py +50 -2
- fal/cli/_utils.py +40 -0
- fal/cli/apps.py +5 -3
- fal/cli/create.py +26 -0
- fal/cli/deploy.py +97 -16
- fal/cli/main.py +2 -2
- fal/cli/parser.py +11 -7
- fal/cli/run.py +12 -1
- fal/cli/runners.py +44 -0
- fal/config.py +23 -0
- fal/container.py +1 -1
- fal/exceptions/__init__.py +7 -1
- fal/exceptions/_base.py +51 -0
- fal/exceptions/_cuda.py +44 -0
- fal/files.py +81 -0
- fal/sdk.py +67 -6
- fal/toolkit/file/file.py +103 -13
- fal/toolkit/file/providers/fal.py +572 -24
- fal/toolkit/file/providers/gcp.py +8 -1
- fal/toolkit/file/providers/r2.py +8 -1
- fal/toolkit/file/providers/s3.py +80 -0
- fal/toolkit/file/types.py +28 -3
- fal/toolkit/image/__init__.py +71 -0
- fal/toolkit/image/image.py +25 -2
- fal/toolkit/image/nsfw_filter/__init__.py +11 -0
- fal/toolkit/image/nsfw_filter/env.py +9 -0
- fal/toolkit/image/nsfw_filter/inference.py +77 -0
- fal/toolkit/image/nsfw_filter/model.py +18 -0
- fal/toolkit/image/nsfw_filter/requirements.txt +4 -0
- fal/toolkit/image/safety_checker.py +107 -0
- fal/toolkit/types.py +140 -0
- fal/toolkit/utils/download_utils.py +4 -0
- fal/toolkit/utils/retry.py +45 -0
- fal/utils.py +20 -4
- fal/workflows.py +10 -4
- {fal-1.2.1.dist-info → fal-1.7.2.dist-info}/METADATA +47 -40
- {fal-1.2.1.dist-info → fal-1.7.2.dist-info}/RECORD +45 -30
- {fal-1.2.1.dist-info → fal-1.7.2.dist-info}/WHEEL +1 -1
- {fal-1.2.1.dist-info → fal-1.7.2.dist-info}/entry_points.txt +0 -0
- {fal-1.2.1.dist-info → fal-1.7.2.dist-info}/top_level.txt +0 -0
fal/cli/apps.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from typing import TYPE_CHECKING
|
|
2
4
|
|
|
3
5
|
from .parser import FalClientParser
|
|
@@ -6,7 +8,7 @@ if TYPE_CHECKING:
|
|
|
6
8
|
from fal.sdk import AliasInfo, ApplicationInfo
|
|
7
9
|
|
|
8
10
|
|
|
9
|
-
def _apps_table(apps: list[
|
|
11
|
+
def _apps_table(apps: list[AliasInfo]):
|
|
10
12
|
from rich.table import Table
|
|
11
13
|
|
|
12
14
|
table = Table()
|
|
@@ -56,7 +58,7 @@ def _add_list_parser(subparsers, parents):
|
|
|
56
58
|
parser.set_defaults(func=_list)
|
|
57
59
|
|
|
58
60
|
|
|
59
|
-
def _app_rev_table(revs: list[
|
|
61
|
+
def _app_rev_table(revs: list[ApplicationInfo]):
|
|
60
62
|
from rich.table import Table
|
|
61
63
|
|
|
62
64
|
table = Table()
|
|
@@ -219,7 +221,7 @@ def _runners(args):
|
|
|
219
221
|
str(runner.in_flight_requests),
|
|
220
222
|
(
|
|
221
223
|
"N/A (active)"
|
|
222
|
-
if
|
|
224
|
+
if runner.expiration_countdown is None
|
|
223
225
|
else f"{runner.expiration_countdown}s"
|
|
224
226
|
),
|
|
225
227
|
f"{runner.uptime} ({runner.uptime.total_seconds()}s)",
|
fal/cli/create.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
PROJECT_TYPES = ["app"]
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _create_project(project_type: str):
|
|
5
|
+
from cookiecutter.main import cookiecutter
|
|
6
|
+
|
|
7
|
+
cookiecutter("https://github.com/fal-ai/cookiecutter-fal.git")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def add_parser(main_subparsers, parents):
|
|
11
|
+
apps_help = "Create fal applications."
|
|
12
|
+
parser = main_subparsers.add_parser(
|
|
13
|
+
"create",
|
|
14
|
+
description=apps_help,
|
|
15
|
+
help=apps_help,
|
|
16
|
+
parents=parents,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
metavar="project_type",
|
|
21
|
+
choices=PROJECT_TYPES,
|
|
22
|
+
help="Type of project to create.",
|
|
23
|
+
dest="project_type",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
parser.set_defaults(func=_create_project)
|
fal/cli/deploy.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
from collections import namedtuple
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from typing import Literal, Optional, Tuple, Union
|
|
4
5
|
|
|
6
|
+
from ._utils import get_app_data_from_toml, is_app_name
|
|
5
7
|
from .parser import FalClientParser, RefAction
|
|
6
8
|
|
|
7
9
|
User = namedtuple("User", ["user_id", "username"])
|
|
@@ -60,11 +62,18 @@ def _get_user() -> User:
|
|
|
60
62
|
raise FalServerlessError(f"Could not parse the user data: {e}")
|
|
61
63
|
|
|
62
64
|
|
|
63
|
-
def
|
|
65
|
+
def _deploy_from_reference(
|
|
66
|
+
app_ref: Tuple[Optional[Union[Path, str]], ...],
|
|
67
|
+
app_name: str,
|
|
68
|
+
args,
|
|
69
|
+
auth: Optional[Literal["public", "shared", "private"]] = None,
|
|
70
|
+
deployment_strategy: Optional[Literal["recreate", "rolling"]] = None,
|
|
71
|
+
no_scale: bool = False,
|
|
72
|
+
):
|
|
64
73
|
from fal.api import FalServerlessError, FalServerlessHost
|
|
65
74
|
from fal.utils import load_function_from
|
|
66
75
|
|
|
67
|
-
file_path, func_name =
|
|
76
|
+
file_path, func_name = app_ref
|
|
68
77
|
if file_path is None:
|
|
69
78
|
# Try to find a python file in the current directory
|
|
70
79
|
options = list(Path(".").glob("*.py"))
|
|
@@ -77,22 +86,28 @@ def _deploy(args):
|
|
|
77
86
|
)
|
|
78
87
|
|
|
79
88
|
[file_path] = options
|
|
80
|
-
file_path = str(file_path)
|
|
89
|
+
file_path = str(file_path) # type: ignore
|
|
81
90
|
|
|
82
91
|
user = _get_user()
|
|
83
92
|
host = FalServerlessHost(args.host)
|
|
84
|
-
|
|
93
|
+
loaded = load_function_from(
|
|
85
94
|
host,
|
|
86
|
-
file_path,
|
|
87
|
-
func_name,
|
|
95
|
+
file_path, # type: ignore
|
|
96
|
+
func_name, # type: ignore
|
|
88
97
|
)
|
|
89
|
-
|
|
98
|
+
isolated_function = loaded.function
|
|
99
|
+
app_name = app_name or loaded.app_name # type: ignore
|
|
100
|
+
app_auth = auth or loaded.app_auth or "private"
|
|
101
|
+
deployment_strategy = deployment_strategy or "recreate"
|
|
102
|
+
|
|
90
103
|
app_id = host.register(
|
|
91
104
|
func=isolated_function.func,
|
|
92
105
|
options=isolated_function.options,
|
|
93
106
|
application_name=app_name,
|
|
94
|
-
application_auth_mode=
|
|
107
|
+
application_auth_mode=app_auth,
|
|
95
108
|
metadata=isolated_function.options.host.get("metadata", {}),
|
|
109
|
+
deployment_strategy=deployment_strategy,
|
|
110
|
+
scale=not no_scale,
|
|
96
111
|
)
|
|
97
112
|
|
|
98
113
|
if app_id:
|
|
@@ -105,12 +120,47 @@ def _deploy(args):
|
|
|
105
120
|
"Registered a new revision for function "
|
|
106
121
|
f"'{app_name}' (revision='{app_id}')."
|
|
107
122
|
)
|
|
108
|
-
args.console.print(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
123
|
+
args.console.print("Playground:")
|
|
124
|
+
for endpoint in loaded.endpoints:
|
|
125
|
+
args.console.print(
|
|
126
|
+
f"\thttps://fal.ai/models/{user.username}/{app_name}{endpoint}"
|
|
127
|
+
)
|
|
128
|
+
args.console.print("Endpoints:")
|
|
129
|
+
for endpoint in loaded.endpoints:
|
|
130
|
+
args.console.print(
|
|
131
|
+
f"\thttps://{gateway_host}/{user.username}/{app_name}{endpoint}"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _deploy(args):
|
|
136
|
+
# my-app
|
|
137
|
+
if is_app_name(args.app_ref):
|
|
138
|
+
# we do not allow --app-name and --auth to be used with app name
|
|
139
|
+
if args.app_name or args.auth:
|
|
140
|
+
raise ValueError("Cannot use --app-name or --auth with app name reference.")
|
|
141
|
+
|
|
142
|
+
app_name = args.app_ref[0]
|
|
143
|
+
app_ref, app_auth, app_deployment_strategy, app_no_scale = (
|
|
144
|
+
get_app_data_from_toml(app_name)
|
|
113
145
|
)
|
|
146
|
+
file_path, func_name = RefAction.split_ref(app_ref)
|
|
147
|
+
|
|
148
|
+
# path/to/myfile.py::MyApp
|
|
149
|
+
else:
|
|
150
|
+
file_path, func_name = args.app_ref
|
|
151
|
+
app_name = args.app_name
|
|
152
|
+
app_auth = args.auth
|
|
153
|
+
app_deployment_strategy = args.strategy
|
|
154
|
+
app_no_scale = args.no_scale
|
|
155
|
+
|
|
156
|
+
_deploy_from_reference(
|
|
157
|
+
(file_path, func_name),
|
|
158
|
+
app_name,
|
|
159
|
+
args,
|
|
160
|
+
app_auth,
|
|
161
|
+
app_deployment_strategy,
|
|
162
|
+
app_no_scale,
|
|
163
|
+
)
|
|
114
164
|
|
|
115
165
|
|
|
116
166
|
def add_parser(main_subparsers, parents):
|
|
@@ -121,14 +171,22 @@ def add_parser(main_subparsers, parents):
|
|
|
121
171
|
raise argparse.ArgumentTypeError(f"{option} is not a auth option")
|
|
122
172
|
return option
|
|
123
173
|
|
|
124
|
-
deploy_help =
|
|
174
|
+
deploy_help = (
|
|
175
|
+
"Deploy a fal application. "
|
|
176
|
+
"If no app reference is provided, the command will look for a "
|
|
177
|
+
"pyproject.toml file with a [tool.fal.apps] section and deploy the "
|
|
178
|
+
"application specified with the provided app name."
|
|
179
|
+
)
|
|
180
|
+
|
|
125
181
|
epilog = (
|
|
126
182
|
"Examples:\n"
|
|
127
183
|
" fal deploy\n"
|
|
128
184
|
" fal deploy path/to/myfile.py\n"
|
|
129
185
|
" fal deploy path/to/myfile.py::MyApp\n"
|
|
130
186
|
" fal deploy path/to/myfile.py::MyApp --app-name myapp --auth public\n"
|
|
187
|
+
" fal deploy my-app\n"
|
|
131
188
|
)
|
|
189
|
+
|
|
132
190
|
parser = main_subparsers.add_parser(
|
|
133
191
|
"deploy",
|
|
134
192
|
parents=[*parents, FalClientParser(add_help=False)],
|
|
@@ -136,22 +194,45 @@ def add_parser(main_subparsers, parents):
|
|
|
136
194
|
help=deploy_help,
|
|
137
195
|
epilog=epilog,
|
|
138
196
|
)
|
|
197
|
+
|
|
139
198
|
parser.add_argument(
|
|
140
199
|
"app_ref",
|
|
141
200
|
nargs="?",
|
|
142
201
|
action=RefAction,
|
|
143
202
|
help=(
|
|
144
|
-
"Application reference.
|
|
203
|
+
"Application reference. Either a file path or a file path and a "
|
|
204
|
+
"function name separated by '::'. If no reference is provided, the "
|
|
205
|
+
"command will look for a pyproject.toml file with a [tool.fal.apps] "
|
|
206
|
+
"section and deploy the application specified with the provided app name.\n"
|
|
207
|
+
"File path example: path/to/myfile.py::MyApp\n"
|
|
208
|
+
"App name example: my-app\n"
|
|
145
209
|
),
|
|
146
210
|
)
|
|
211
|
+
|
|
147
212
|
parser.add_argument(
|
|
148
213
|
"--app-name",
|
|
149
214
|
help="Application name to deploy with.",
|
|
150
215
|
)
|
|
216
|
+
|
|
151
217
|
parser.add_argument(
|
|
152
218
|
"--auth",
|
|
153
219
|
type=valid_auth_option,
|
|
154
|
-
default="private",
|
|
155
220
|
help="Application authentication mode (private, public).",
|
|
156
221
|
)
|
|
222
|
+
parser.add_argument(
|
|
223
|
+
"--strategy",
|
|
224
|
+
choices=["recreate", "rolling"],
|
|
225
|
+
help="Deployment strategy.",
|
|
226
|
+
default="recreate",
|
|
227
|
+
)
|
|
228
|
+
parser.add_argument(
|
|
229
|
+
"--no-scale",
|
|
230
|
+
action="store_true",
|
|
231
|
+
help=(
|
|
232
|
+
"Use min_concurrency/max_concurrency/max_multiplexing from previous "
|
|
233
|
+
"deployment of application with this name, if exists. Otherwise will "
|
|
234
|
+
"use the values from the application code."
|
|
235
|
+
),
|
|
236
|
+
)
|
|
237
|
+
|
|
157
238
|
parser.set_defaults(func=_deploy)
|
fal/cli/main.py
CHANGED
|
@@ -6,7 +6,7 @@ from fal import __version__
|
|
|
6
6
|
from fal.console import console
|
|
7
7
|
from fal.console.icons import CROSS_ICON
|
|
8
8
|
|
|
9
|
-
from . import apps, auth, deploy, doctor, keys, run, secrets
|
|
9
|
+
from . import apps, auth, create, deploy, doctor, keys, run, runners, secrets
|
|
10
10
|
from .debug import debugtools, get_debug_parser
|
|
11
11
|
from .parser import FalParser, FalParserExit
|
|
12
12
|
|
|
@@ -31,7 +31,7 @@ def _get_main_parser() -> argparse.ArgumentParser:
|
|
|
31
31
|
required=True,
|
|
32
32
|
)
|
|
33
33
|
|
|
34
|
-
for cmd in [auth, apps, deploy, run, keys, secrets, doctor]:
|
|
34
|
+
for cmd in [auth, apps, deploy, run, keys, secrets, doctor, create, runners]:
|
|
35
35
|
cmd.add_parser(subparsers, parents)
|
|
36
36
|
|
|
37
37
|
return parser
|
fal/cli/parser.py
CHANGED
|
@@ -14,14 +14,18 @@ class RefAction(argparse.Action):
|
|
|
14
14
|
kwargs.setdefault("default", (None, None))
|
|
15
15
|
super().__init__(*args, **kwargs)
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
@classmethod
|
|
18
|
+
def split_ref(cls, value):
|
|
19
|
+
if isinstance(value, tuple):
|
|
20
|
+
return value
|
|
21
|
+
|
|
22
|
+
if value.find("::") > 1:
|
|
23
|
+
return value.split("::", 1)
|
|
24
24
|
|
|
25
|
+
return value, None
|
|
26
|
+
|
|
27
|
+
def __call__(self, parser, args, values, option_string=None): # noqa: ARG002
|
|
28
|
+
file_path, obj_path = self.split_ref(values)
|
|
25
29
|
setattr(args, self.dest, (file_path, obj_path))
|
|
26
30
|
|
|
27
31
|
|
fal/cli/run.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from ._utils import get_app_data_from_toml, is_app_name
|
|
1
2
|
from .parser import FalClientParser, RefAction
|
|
2
3
|
|
|
3
4
|
|
|
@@ -6,7 +7,17 @@ def _run(args):
|
|
|
6
7
|
from fal.utils import load_function_from
|
|
7
8
|
|
|
8
9
|
host = FalServerlessHost(args.host)
|
|
9
|
-
|
|
10
|
+
|
|
11
|
+
if is_app_name(args.func_ref):
|
|
12
|
+
app_name = args.func_ref[0]
|
|
13
|
+
app_ref, *_ = get_app_data_from_toml(app_name)
|
|
14
|
+
file_path, func_name = RefAction.split_ref(app_ref)
|
|
15
|
+
else:
|
|
16
|
+
file_path, func_name = args.func_ref
|
|
17
|
+
|
|
18
|
+
loaded = load_function_from(host, file_path, func_name)
|
|
19
|
+
|
|
20
|
+
isolated_function = loaded.function
|
|
10
21
|
# let our exc handlers handle UserFunctionException
|
|
11
22
|
isolated_function.reraise = False
|
|
12
23
|
isolated_function()
|
fal/cli/runners.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from .parser import FalClientParser
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _kill(args):
|
|
5
|
+
from fal.sdk import FalServerlessClient
|
|
6
|
+
|
|
7
|
+
client = FalServerlessClient(args.host)
|
|
8
|
+
with client.connect() as connection:
|
|
9
|
+
connection.kill_runner(args.id)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _add_kill_parser(subparsers, parents):
|
|
13
|
+
kill_help = "Kill a runner."
|
|
14
|
+
parser = subparsers.add_parser(
|
|
15
|
+
"kill",
|
|
16
|
+
description=kill_help,
|
|
17
|
+
help=kill_help,
|
|
18
|
+
parents=parents,
|
|
19
|
+
)
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"id",
|
|
22
|
+
help="Runner ID.",
|
|
23
|
+
)
|
|
24
|
+
parser.set_defaults(func=_kill)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def add_parser(main_subparsers, parents):
|
|
28
|
+
runners_help = "Manage fal runners."
|
|
29
|
+
parser = main_subparsers.add_parser(
|
|
30
|
+
"runners",
|
|
31
|
+
description=runners_help,
|
|
32
|
+
help=runners_help,
|
|
33
|
+
parents=parents,
|
|
34
|
+
aliases=["machine"], # backwards compatibility
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
subparsers = parser.add_subparsers(
|
|
38
|
+
title="Commands",
|
|
39
|
+
metavar="command",
|
|
40
|
+
required=True,
|
|
41
|
+
parser_class=FalClientParser,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
_add_kill_parser(subparsers, parents)
|
fal/config.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import tomli
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Config:
|
|
7
|
+
DEFAULT_CONFIG_PATH = "~/.fal/config.toml"
|
|
8
|
+
DEFAULT_PROFILE = "default"
|
|
9
|
+
|
|
10
|
+
def __init__(self):
|
|
11
|
+
self.config_path = os.path.expanduser(
|
|
12
|
+
os.getenv("FAL_CONFIG_PATH", self.DEFAULT_CONFIG_PATH)
|
|
13
|
+
)
|
|
14
|
+
self.profile = os.getenv("FAL_PROFILE", self.DEFAULT_PROFILE)
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
with open(self.config_path, "rb") as file:
|
|
18
|
+
self.config = tomli.load(file)
|
|
19
|
+
except FileNotFoundError:
|
|
20
|
+
self.config = {}
|
|
21
|
+
|
|
22
|
+
def get(self, key):
|
|
23
|
+
return self.config.get(self.profile, {}).get(key)
|
fal/container.py
CHANGED
fal/exceptions/__init__.py
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from ._base import
|
|
3
|
+
from ._base import (
|
|
4
|
+
AppException, # noqa: F401
|
|
5
|
+
FalServerlessException, # noqa: F401
|
|
6
|
+
FieldException, # noqa: F401
|
|
7
|
+
RequestCancelledException, # noqa: F401
|
|
8
|
+
)
|
|
9
|
+
from ._cuda import CUDAOutOfMemoryException # noqa: F401
|
fal/exceptions/_base.py
CHANGED
|
@@ -1,7 +1,58 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
3
5
|
|
|
4
6
|
class FalServerlessException(Exception):
|
|
5
7
|
"""Base exception type for fal Serverless related flows and APIs."""
|
|
6
8
|
|
|
7
9
|
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class AppException(FalServerlessException):
|
|
14
|
+
"""
|
|
15
|
+
Base exception class for application-specific errors.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
message: A descriptive message explaining the error.
|
|
19
|
+
status_code: The HTTP status code associated with the error.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
message: str
|
|
23
|
+
status_code: int
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class FieldException(FalServerlessException):
|
|
28
|
+
"""Exception raised for errors related to specific fields.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
field: The field that caused the error.
|
|
32
|
+
message: A descriptive message explaining the error.
|
|
33
|
+
status_code: The HTTP status code associated with the error. Defaults to 422
|
|
34
|
+
type: The type of error. Defaults to "value_error"
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
field: str
|
|
38
|
+
message: str
|
|
39
|
+
status_code: int = 422
|
|
40
|
+
type: str = "value_error"
|
|
41
|
+
|
|
42
|
+
def to_pydantic_format(self) -> dict[str, list[dict]]:
|
|
43
|
+
return dict(
|
|
44
|
+
detail=[
|
|
45
|
+
{
|
|
46
|
+
"loc": ["body", self.field],
|
|
47
|
+
"msg": self.message,
|
|
48
|
+
"type": self.type,
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class RequestCancelledException(FalServerlessException):
|
|
56
|
+
"""Exception raised when the request is cancelled by the client."""
|
|
57
|
+
|
|
58
|
+
message: str = "Request cancelled by the client."
|
fal/exceptions/_cuda.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from ._base import AppException
|
|
6
|
+
|
|
7
|
+
# PyTorch error message for out of memory
|
|
8
|
+
_CUDA_OOM_MESSAGE = "CUDA error: out of memory"
|
|
9
|
+
|
|
10
|
+
# Special status code for CUDA out of memory errors
|
|
11
|
+
_CUDA_OOM_STATUS_CODE = 503
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class CUDAOutOfMemoryException(AppException):
|
|
16
|
+
"""Exception raised when a CUDA operation runs out of memory."""
|
|
17
|
+
|
|
18
|
+
message: str = _CUDA_OOM_MESSAGE
|
|
19
|
+
status_code: int = _CUDA_OOM_STATUS_CODE
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# based on https://github.com/Lightning-AI/pytorch-lightning/blob/37e04d075a5532c69b8ac7457795b4345cca30cc/src/lightning/pytorch/utilities/memory.py#L49
|
|
23
|
+
def _is_cuda_oom_exception(exception: BaseException) -> bool:
|
|
24
|
+
return _is_cuda_out_of_memory(exception) or _is_cudnn_snafu(exception)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# based on https://github.com/BlackHC/toma/blob/master/toma/torch_cuda_memory.py
|
|
28
|
+
def _is_cuda_out_of_memory(exception: BaseException) -> bool:
|
|
29
|
+
return (
|
|
30
|
+
isinstance(exception, RuntimeError)
|
|
31
|
+
and len(exception.args) == 1
|
|
32
|
+
and "CUDA" in exception.args[0]
|
|
33
|
+
and "out of memory" in exception.args[0]
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# based on https://github.com/BlackHC/toma/blob/master/toma/torch_cuda_memory.py
|
|
38
|
+
def _is_cudnn_snafu(exception: BaseException) -> bool:
|
|
39
|
+
# For/because of https://github.com/pytorch/pytorch/issues/4107
|
|
40
|
+
return (
|
|
41
|
+
isinstance(exception, RuntimeError)
|
|
42
|
+
and len(exception.args) == 1
|
|
43
|
+
and "cuDNN error: CUDNN_STATUS_NOT_SUPPORTED." in exception.args[0]
|
|
44
|
+
)
|
fal/files.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from functools import lru_cache
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Dict, Optional, Sequence, Tuple, Union
|
|
4
|
+
|
|
5
|
+
import tomli
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@lru_cache
|
|
9
|
+
def _load_toml(path: Union[Path, str]) -> Dict[str, Any]:
|
|
10
|
+
with open(path, "rb") as f:
|
|
11
|
+
return tomli.load(f)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@lru_cache
|
|
15
|
+
def _cached_resolve(path: Path) -> Path:
|
|
16
|
+
return path.resolve()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@lru_cache
|
|
20
|
+
def find_project_root(srcs: Optional[Sequence[str]]) -> Tuple[Path, str]:
|
|
21
|
+
"""Return a directory containing .git, or pyproject.toml.
|
|
22
|
+
|
|
23
|
+
That directory will be a common parent of all files and directories
|
|
24
|
+
passed in `srcs`.
|
|
25
|
+
|
|
26
|
+
If no directory in the tree contains a marker that would specify it's the
|
|
27
|
+
project root, the root of the file system is returned.
|
|
28
|
+
|
|
29
|
+
Returns a two-tuple with the first element as the project root path and
|
|
30
|
+
the second element as a string describing the method by which the
|
|
31
|
+
project root was discovered.
|
|
32
|
+
"""
|
|
33
|
+
if not srcs:
|
|
34
|
+
srcs = [str(_cached_resolve(Path.cwd()))]
|
|
35
|
+
|
|
36
|
+
path_srcs = [_cached_resolve(Path(Path.cwd(), src)) for src in srcs]
|
|
37
|
+
|
|
38
|
+
# A list of lists of parents for each 'src'. 'src' is included as a
|
|
39
|
+
# "parent" of itself if it is a directory
|
|
40
|
+
src_parents = [
|
|
41
|
+
list(path.parents) + ([path] if path.is_dir() else []) for path in path_srcs
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
common_base = max(
|
|
45
|
+
set.intersection(*(set(parents) for parents in src_parents)),
|
|
46
|
+
key=lambda path: path.parts,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
for directory in (common_base, *common_base.parents):
|
|
50
|
+
if (directory / ".git").exists():
|
|
51
|
+
return directory, ".git directory"
|
|
52
|
+
|
|
53
|
+
if (directory / "pyproject.toml").is_file():
|
|
54
|
+
pyproject_toml = _load_toml(directory / "pyproject.toml")
|
|
55
|
+
if "fal" in pyproject_toml.get("tool", {}):
|
|
56
|
+
return directory, "pyproject.toml"
|
|
57
|
+
|
|
58
|
+
return directory, "file system root"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def find_pyproject_toml(
|
|
62
|
+
path_search_start: Optional[Tuple[str, ...]] = None,
|
|
63
|
+
) -> Optional[str]:
|
|
64
|
+
"""Find the absolute filepath to a pyproject.toml if it exists"""
|
|
65
|
+
path_project_root, _ = find_project_root(path_search_start)
|
|
66
|
+
path_pyproject_toml = path_project_root / "pyproject.toml"
|
|
67
|
+
|
|
68
|
+
if path_pyproject_toml.is_file():
|
|
69
|
+
return str(path_pyproject_toml)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def parse_pyproject_toml(path_config: str) -> Dict[str, Any]:
|
|
73
|
+
"""Parse a pyproject toml file, pulling out relevant parts for fal.
|
|
74
|
+
|
|
75
|
+
If parsing fails, will raise a tomli.TOMLDecodeError.
|
|
76
|
+
"""
|
|
77
|
+
pyproject_toml = _load_toml(path_config)
|
|
78
|
+
config: Dict[str, Any] = pyproject_toml.get("tool", {}).get("fal", {})
|
|
79
|
+
config = {k.replace("--", "").replace("-", "_"): v for k, v in config.items()}
|
|
80
|
+
|
|
81
|
+
return config
|