Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds warnings if automounts seem to be required #2667

Merged
merged 49 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
304d09d
Adds warnings if automounts seem to be required
freider Dec 16, 2024
956c439
Merge remote-tracking branch 'origin/main' into freider/missing-packa…
freider Jan 9, 2025
631fd54
Fix
freider Jan 9, 2025
a887cf1
Adds test etc
freider Jan 10, 2025
0dbac39
comment
freider Jan 10, 2025
0cf5340
Change automounting logic to exclude entrypoint package from auto-mou…
freider Jan 10, 2025
261932b
Remove prints
freider Jan 10, 2025
0315a22
Merge remote-tracking branch 'origin/main' into freider/missing-packa…
freider Jan 10, 2025
3a5fcae
Copyright
freider Jan 10, 2025
d3d0a42
Settings refactor wip
freider Jan 17, 2025
1f04e22
Merge remote-tracking branch 'origin/main' into freider/auto-add-conf…
freider Jan 22, 2025
8e14192
Simplify
freider Jan 22, 2025
3e99220
Merge remote-tracking branch 'origin/main' into freider/auto-add-conf…
freider Jan 23, 2025
b74ac27
Fixed test
freider Jan 24, 2025
f2f92c1
Update test assertion to reflect new behavior with automount=0
freider Jan 24, 2025
859859e
Change to enum.Enum
freider Jan 24, 2025
35d1f0a
Fix circular import
freider Jan 24, 2025
a57bf58
Merge remote-tracking branch 'origin/main' into freider/auto-add-conf…
freider Jan 24, 2025
0573ece
Fix unnecessary comparison
freider Jan 24, 2025
e6da2fd
Fix for config only working with eval()-values
freider Jan 24, 2025
a2a7647
add type ignore for __notes__
freider Jan 24, 2025
14db34f
Test assertion
freider Jan 24, 2025
6f7e792
Merge branch 'freider/auto-add-configuration' into freider/missing-pa…
freider Jan 24, 2025
68584cc
Updated message
freider Jan 27, 2025
498c947
Updated values of include_source attribute
freider Jan 27, 2025
f5190b6
types
freider Jan 27, 2025
e39d6b2
Merge branch 'freider/auto-add-configuration' into freider/missing-pa…
freider Jan 27, 2025
b3dc36b
Remove comment
freider Jan 27, 2025
5c48fc5
Blah
freider Jan 27, 2025
0851be3
move
freider Jan 27, 2025
ce7e700
Restore automount config parsing
freider Jan 27, 2025
18f30a5
port logic
freider Jan 27, 2025
8d592a9
Updated error message
freider Jan 27, 2025
0236192
Merge branch 'freider/auto-add-configuration' into freider/missing-pa…
freider Jan 27, 2025
bf2b590
Bump minor version
freider Jan 27, 2025
9284f1c
Merge branch 'freider/auto-add-configuration' into freider/missing-pa…
freider Jan 27, 2025
10fe5dc
Don't accept legacy as an explicit value
freider Jan 28, 2025
72d5f23
Change back to including non-py-files in the entrypoint package mount
freider Jan 28, 2025
e0b223e
Comment
freider Jan 28, 2025
a48bfbf
Merge branch 'freider/auto-add-configuration' into freider/missing-pa…
freider Jan 28, 2025
fb7db5b
Merge remote-tracking branch 'origin/main' into freider/missing-packa…
freider Jan 30, 2025
b084a55
Fix tests
freider Jan 30, 2025
ec0ad9a
Reapply changes to _functions
freider Jan 31, 2025
7384abf
Apply functions from main
freider Jan 31, 2025
2f962db
Merge remote-tracking branch 'origin/main' into freider/missing-packa…
freider Jan 31, 2025
341abc4
Update warning text
freider Jan 31, 2025
c3ef2da
Revert extend addition
freider Feb 3, 2025
9766ec8
Incorporated michael's feedback
freider Feb 3, 2025
6d0c18a
Date
freider Feb 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 31 additions & 8 deletions modal/_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
)
from .gpu import GPU_T, parse_gpu_config
from .image import _Image
from .mount import _get_client_mount, _Mount, get_auto_mounts
from .mount import _get_client_mount, _Mount, get_sys_modules_mounts
from .network_file_system import _NetworkFileSystem, network_file_system_mount_protos
from .output import _get_output_manager
from .parallel_map import (
Expand Down Expand Up @@ -434,7 +434,7 @@ def _bind_method(
return fun

@staticmethod
def from_args(
def from_local(
info: FunctionInfo,
app,
image: _Image,
Expand Down Expand Up @@ -501,18 +501,41 @@ def from_args(
if include_source_mode != IncludeSourceMode.INCLUDE_NOTHING:
entrypoint_mounts = info.get_entrypoint_mount()
else:
entrypoint_mounts = []
entrypoint_mounts = {}

all_mounts = [
_get_client_mount(),
*explicit_mounts,
*entrypoint_mounts,
*entrypoint_mounts.values(),
]

if include_source_mode is IncludeSourceMode.INCLUDE_FIRST_PARTY:
# TODO(elias): if using INCLUDE_FIRST_PARTY *and* mounts are added that haven't already been
# added to the image via add_local_python_source
all_mounts += get_auto_mounts()
auto_mounts = get_sys_modules_mounts()
# don't need to add entrypoint modules to automounts:
for entrypoint_module in entrypoint_mounts:
auto_mounts.pop(entrypoint_module, None)

warn_missing_modules = set(auto_mounts.keys()) - image._added_python_source_set

if warn_missing_modules:
python_stringified_modules = ", ".join(f'"{mod}"' for mod in sorted(warn_missing_modules))
deprecation_warning(
(2024, 1, 28),
(
"Automatic adding of local python source will be deprecated in the future.\n"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mwaskom let me know if you have ideas for other ways of phrasing this, and if we should mention the include_source option or not (I'm leaning towards no since it's not typically needed)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think that include_source is not relevant at this stage of the deprecation process; advising users on how to update their Image definitions

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested rephrasing of the prose:

Modal will stop implicitly adding local Python modules to the Image ("automounting") in a future update. The following modules need to be explicitly added for future compatibility:

f"Make sure you have explicitly added the source for the following modules to the image "
f"used by `{info.function_name}`:\n"
)
+ ", ".join(sorted(warn_missing_modules))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: can we make this a bulleted list? It looks visually strange for a single module imo:

╭─ Modal Deprecation Warning (2024-01-31) ───────────────────────────────────────────────────────────────╮
│ Automatic adding of local python source will be deprecated in the future.                              │
│ Make sure you have explicitly added the source for the following modules to the Image used by `f`:     │
│ helper                                                                                                 │
│                                                                                                        │
│ e.g.:                                                                                                  │

+ "\n\n"
+ (
"This can be using `Image.add_local_python_source`, e.g.:\n"
f"image_with_source = Image.debian_slim().add_local_python_source"
f"({python_stringified_modules})"
),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a line like "For more information, see https://modal.com/docs/guide/modal-1-0-migration"? When users start seeing all the new deprecation warnings I think it will be helpful to call their attention to the idea that this is "one last hurrah" to manage their annoyance :D.

We can also add a more verbose explanation of what's going on and the various options for ameliorating it there. I can work on that.

pending=True,
)
all_mounts += auto_mounts.values()
else:
# skip any mount introspection/logic inside containers, since the function
# should already be hydrated
Expand Down Expand Up @@ -561,7 +584,7 @@ def from_args(
for k, pf in build_functions:
build_function = pf.raw_f
snapshot_info = FunctionInfo(build_function, user_cls=info.user_cls)
snapshot_function = _Function.from_args(
snapshot_function = _Function.from_local(
snapshot_info,
app=None,
image=image,
Expand Down
26 changes: 14 additions & 12 deletions modal/_utils/function_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ def class_parameter_info(self) -> api_pb2.ClassParameterInfo:
format=api_pb2.ClassParameterInfo.PARAM_SERIALIZATION_FORMAT_PROTO, schema=modal_parameters
)

def get_entrypoint_mount(self) -> list[_Mount]:
def get_entrypoint_mount(self) -> dict[str, _Mount]:
"""
Includes:
* Implicit mount of the function itself (the module or package that the function is part of)
Expand All @@ -322,7 +322,7 @@ def get_entrypoint_mount(self) -> list[_Mount]:
"""
if self.is_serialized():
# Don't auto-mount anything for serialized functions (including notebooks)
return []
return {}

# make sure the function's own entrypoint is included:
if self._type == FunctionInfoType.PACKAGE:
Expand All @@ -331,21 +331,23 @@ def get_entrypoint_mount(self) -> list[_Mount]:
# includes non-.py files, since we'll want to migrate to .py-only
# soon to get it consistent with the `add_local_python_source()`
# defaults.
return [_Mount._from_local_python_packages(top_level_package)]
return {top_level_package: _Mount._from_local_python_packages(top_level_package)}
elif self._type == FunctionInfoType.FILE:
remote_path = ROOT_DIR / Path(self._file).name
# TODO: inspect if this file is already included as part of
# a package mount, and skip it + reference that package
# instead if that's the case. This avoids possible module
# duplication bugs
module_file = Path(self._file)
container_module_name = module_file.stem
remote_path = ROOT_DIR / module_file.name
if not _is_modal_path(remote_path):
# TODO: inspect if this file is already included as part of
# a package mount, and skip it + reference that package
# instead if that's the case. This avoids possible module
# duplication bugs
return [
_Mount._from_local_file(
return {
container_module_name: _Mount._from_local_file(
self._file,
remote_path=remote_path,
)
]
return [] # this should never be reached...
}
return {} # this should never be reached...

def get_tag(self):
return self.function_name
Expand Down
4 changes: 2 additions & 2 deletions modal/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -724,7 +724,7 @@ def f(self, x):
raise InvalidError("`region` and `_experimental_scheduler_placement` cannot be used together")
scheduler_placement = SchedulerPlacement(region=region)

function = _Function.from_args(
function = _Function.from_local(
info,
app=self,
image=image,
Expand Down Expand Up @@ -859,7 +859,7 @@ def wrapper(user_cls: CLS_T) -> CLS_T:

info = FunctionInfo(None, serialized=serialized, user_cls=user_cls)

cls_func = _Function.from_args(
cls_func = _Function.from_local(
info,
app=self,
image=image or self._get_default_image(),
Expand Down
72 changes: 35 additions & 37 deletions modal/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,12 +402,14 @@ class _Image(_Object, type_prefix="im"):
_deferred_mounts: Sequence[
_Mount
] # added as mounts on any container referencing the Image, see `def _mount_layers`
_added_python_source_set: frozenset[str] # used to warn about missing mounts during auto-mount deprecation
_metadata: Optional[api_pb2.ImageMetadata] = None # set on hydration, private for now

def _initialize_from_empty(self):
self.inside_exceptions = []
self._serve_mounts = frozenset()
self._deferred_mounts = ()
self._added_python_source_set = frozenset()
self.force_build = False

def _initialize_from_other(self, other: "_Image"):
Expand All @@ -416,6 +418,7 @@ def _initialize_from_other(self, other: "_Image"):
self.force_build = other.force_build
self._serve_mounts = other._serve_mounts
self._deferred_mounts = other._deferred_mounts
self._added_python_source_set = other._added_python_source_set

def _hydrate_metadata(self, metadata: Optional[Message]):
env_image_id = config.get("image_id") # set as an env var in containers
Expand Down Expand Up @@ -657,9 +660,18 @@ async def _load(self: _Image, resolver: Resolver, existing_object_id: Optional[s
self._serve_mounts = frozenset(local_mounts)

rep = f"Image({dockerfile_function})"
obj = _Image._from_loader(_load, rep, deps=_deps)
obj.force_build = force_build
return obj
img = _Image._from_loader(_load, rep, deps=_deps)
img.force_build = force_build
return img

def _extend(self, **kwargs) -> "_Image":
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we used to have this as a public (?) method, but removed/deprecated it. Now I needed a new private version of it to make it more ergonomic to transfer attributes from parent layers to children (_added_python_source_set in this case)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there some reason Image._from_args can't automatically transfer the added source set from the provided base Image?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't seem to come up with one now, but I have a feeling there was something that made me go this route back in december 😬

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted it and managed to get it working now. Also found a bug and added a test for that in the process

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, seems not ideal to have the logic required to extend an Image distributed over two methods instead of just one.

"""Internal use only - helper method to create a new Image with self as base

Transfers required static attributes to the new layer as expected.
"""
img = _Image._from_args(base_images={"base": self}, **kwargs)
img._added_python_source_set = self._added_python_source_set
return img

def copy_mount(self, mount: _Mount, remote_path: Union[str, Path] = ".") -> "_Image":
"""
Expand All @@ -686,8 +698,7 @@ def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
commands = ["FROM base", f"COPY . {remote_path}"] # copy everything from the supplied mount
return DockerfileSpec(commands=commands, context_files={})

return _Image._from_args(
base_images={"base": self},
return self._extend(
dockerfile_function=build_dockerfile,
context_mount_function=lambda: mount,
)
Expand Down Expand Up @@ -801,8 +812,7 @@ def copy_local_file(self, local_path: Union[str, Path], remote_path: Union[str,
def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
return DockerfileSpec(commands=["FROM base", f"COPY {basename} {remote_path}"], context_files={})

return _Image._from_args(
base_images={"base": self},
return self._extend(
dockerfile_function=build_dockerfile,
context_mount_function=lambda: _Mount._from_local_file(local_path, remote_path=f"/{basename}"),
)
Expand Down Expand Up @@ -843,7 +853,9 @@ def add_local_python_source(
```
"""
mount = _Mount._from_local_python_packages(*modules, ignore=ignore)
return self._add_mount_layer_or_copy(mount, copy=copy)
img = self._add_mount_layer_or_copy(mount, copy=copy)
img._added_python_source_set |= set(modules)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the key part of this change - track all add_local_python_source modules on the Image objects so we can inspect it later when the automount logic executes to see which modules are missing. It's not perfect, but people can get rid of false positive warnings by setting include_source to any boolean value (with the expectation that they know what they are doing)

return img

def copy_local_dir(
self,
Expand Down Expand Up @@ -908,8 +920,7 @@ def copy_local_dir(
def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
return DockerfileSpec(commands=["FROM base", f"COPY . {remote_path}"], context_files={})

return _Image._from_args(
base_images={"base": self},
return self._extend(
dockerfile_function=build_dockerfile,
context_mount_function=lambda: _Mount._add_local_dir(
Path(local_path), PurePosixPath("/"), ignore=_ignore_fn(ignore)
Expand Down Expand Up @@ -988,8 +999,7 @@ def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
return DockerfileSpec(commands=commands, context_files={})

gpu_config = parse_gpu_config(gpu)
return _Image._from_args(
base_images={"base": self},
return self._extend(
dockerfile_function=build_dockerfile,
force_build=self.force_build or force_build,
gpu_config=gpu_config,
Expand Down Expand Up @@ -1089,8 +1099,7 @@ def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:

gpu_config = parse_gpu_config(gpu)

return _Image._from_args(
base_images={"base": self},
return self._extend(
dockerfile_function=build_dockerfile,
secrets=secrets,
gpu_config=gpu_config,
Expand Down Expand Up @@ -1129,8 +1138,7 @@ def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
commands = [cmd.strip() for cmd in commands]
return DockerfileSpec(commands=commands, context_files=context_files)

return _Image._from_args(
base_images={"base": self},
return self._extend(
dockerfile_function=build_dockerfile,
force_build=self.force_build or force_build,
gpu_config=parse_gpu_config(gpu),
Expand Down Expand Up @@ -1191,8 +1199,7 @@ def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:

return DockerfileSpec(commands=commands, context_files={})

return _Image._from_args(
base_images={"base": self},
return self._extend(
dockerfile_function=build_dockerfile,
force_build=self.force_build or force_build,
secrets=secrets,
Expand Down Expand Up @@ -1272,8 +1279,7 @@ def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
]
return DockerfileSpec(commands=commands, context_files=context_files)

return _Image._from_args(
base_images={"base": self},
return self._extend(
dockerfile_function=build_dockerfile,
force_build=self.force_build or force_build,
secrets=secrets,
Expand Down Expand Up @@ -1346,8 +1352,7 @@ def dockerfile_commands(
def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
return DockerfileSpec(commands=["FROM base", *cmds], context_files=context_files)

return _Image._from_args(
base_images={"base": self},
return self._extend(
dockerfile_function=build_dockerfile,
secrets=secrets,
gpu_config=parse_gpu_config(gpu),
Expand Down Expand Up @@ -1394,8 +1399,7 @@ def run_commands(
def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
return DockerfileSpec(commands=["FROM base"] + [f"RUN {cmd}" for cmd in cmds], context_files={})

return _Image._from_args(
base_images={"base": self},
return self._extend(
dockerfile_function=build_dockerfile,
secrets=secrets,
gpu_config=parse_gpu_config(gpu),
Expand Down Expand Up @@ -1505,8 +1509,7 @@ def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
context_files = {} if spec_file is None else {remote_spec_file: os.path.expanduser(spec_file)}
return DockerfileSpec(commands=commands, context_files=context_files)

return _Image._from_args(
base_images={"base": self},
return self._extend(
dockerfile_function=build_dockerfile,
force_build=self.force_build or force_build,
secrets=secrets,
Expand Down Expand Up @@ -1820,8 +1823,7 @@ def build_dockerfile_python(version: ImageBuilderVersion) -> DockerfileSpec:
context_files = {CONTAINER_REQUIREMENTS_PATH: requirements_path}
return DockerfileSpec(commands=commands, context_files=context_files)

return _Image._from_args(
base_images={"base": base_image},
return base_image._extend(
dockerfile_function=build_dockerfile_python,
context_mount_function=add_python_mount,
force_build=force_build,
Expand Down Expand Up @@ -1888,8 +1890,7 @@ def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
]
return DockerfileSpec(commands=commands, context_files={})

return _Image._from_args(
base_images={"base": self},
return self._extend(
dockerfile_function=build_dockerfile,
force_build=self.force_build or force_build,
gpu_config=parse_gpu_config(gpu),
Expand Down Expand Up @@ -1954,7 +1955,7 @@ def my_build_function():

info = FunctionInfo(raw_f)

function = _Function.from_args(
function = _Function.from_local(
info,
app=None,
image=self, # type: ignore[reportArgumentType] # TODO: probably conflict with type stub?
Expand All @@ -1980,8 +1981,7 @@ def my_build_function():
build_function_input = api_pb2.FunctionInput(args=args_serialized, data_format=api_pb2.DATA_FORMAT_PICKLE)
else:
build_function_input = None
return _Image._from_args(
base_images={"base": self},
return self._extend(
build_function=function,
build_function_input=build_function_input,
force_build=self.force_build or force_build,
Expand All @@ -2004,8 +2004,7 @@ def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
commands = ["FROM base"] + [f"ENV {key}={shlex.quote(val)}" for (key, val) in vars.items()]
return DockerfileSpec(commands=commands, context_files={})

return _Image._from_args(
base_images={"base": self},
return self._extend(
dockerfile_function=build_dockerfile,
)

Expand All @@ -2028,8 +2027,7 @@ def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
commands = ["FROM base", f"WORKDIR {shlex.quote(str(path))}"]
return DockerfileSpec(commands=commands, context_files={})

return _Image._from_args(
base_images={"base": self},
return self._extend(
dockerfile_function=build_dockerfile,
)

Expand Down
6 changes: 3 additions & 3 deletions modal/mount.py
Original file line number Diff line number Diff line change
Expand Up @@ -821,14 +821,14 @@ def _is_modal_path(remote_path: PurePosixPath):
return False


def get_auto_mounts() -> list[_Mount]:
def get_sys_modules_mounts() -> dict[str, _Mount]:
"""mdmd:hidden

Auto-mount local modules that have been imported in global scope.
This may or may not include the "entrypoint" of the function as well, depending on how modal is invoked
Note: sys.modules may change during the iteration
"""
auto_mounts = []
auto_mounts = {}
top_level_modules = []
skip_prefixes = set()
for name, module in sorted(sys.modules.items(), key=lambda kv: len(kv[0])):
Expand Down Expand Up @@ -858,6 +858,6 @@ def get_auto_mounts() -> list[_Mount]:
# skip any module that has paths in SYS_PREFIXES, or would overwrite the modal Package in the container
break
else:
auto_mounts.append(potential_mount)
auto_mounts[module_name] = potential_mount

return auto_mounts
Loading
Loading