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

Decorator-based lifecycle hooks #1136

Merged
merged 14 commits into from
Dec 21, 2023
Merged

Conversation

erikbern
Copy link
Contributor

@erikbern erikbern commented Dec 18, 2023

This adds @build, @enter, and @exit which are decorators you can apply to class methods to trigger lifecycle hooks. Also a bunch of general refactoring of _PartialFunction code to support stacking decorators and generalize how we grab methods from user classes.

Synopsis:

@stub.cls()
class MyClass:
    @build()
    @enter()
    def my_enter(self):
        print("I'm entering")

    @method()
    def fun(self):
        ...

In the example above, the my_enter method will run both as a part of building the image, and as a part of every container initialization, showcasing the ability to stack decorators. This new syntax intends to solve both (1) an easier way to "auto-initialize" images (2) a more ergonomic replacement for most/all cases of Image.run_function.

Big delta but a lot of it is tests. And we should be able to retire a bit of code later once we deprecate the "legacy" methods. I didn't add any deprecation warnings at this point since this is complex change so we might want to take our time and update docs and examples to verify that this approach is the right one.

@erikbern erikbern force-pushed the erikbern/decorator-lifecycle-hooks branch from 52c946e to 25e0a30 Compare December 18, 2023 04:36
@erikbern erikbern force-pushed the erikbern/decorator-lifecycle-hooks branch 5 times, most recently from 6469f1e to 5953f1f Compare December 18, 2023 15:21
@erikbern erikbern force-pushed the erikbern/decorator-lifecycle-hooks branch from 5953f1f to dcf00e2 Compare December 18, 2023 15:34
@luiscape
Copy link
Member

How should we handle the checkpointing case? Is that @build? Would it be worth it to add an additional method that only run during a checkpointing stage (perhaps @warmup)?

@erikbern
Copy link
Contributor Author

How should we handle the checkpointing case? Is that @build? Would it be worth it to add an additional method that only run during a checkpointing stage (perhaps @warmup)?

Yeah, that's exactly right - I think we should add some more decorators to support more granular hooks, e.g. @enter_cpu and @enter_gpu or something like that. I don't have any strong feelings about the exact terminology. But the point is we can make a lot of the performance tricks a lot cleaner in code (e.g. use @build instead of run_commands, use @enter_cpu instead of the with image.imports(): trick for checkpoints)

@erikbern erikbern force-pushed the erikbern/decorator-lifecycle-hooks branch from 2e0efbd to cee016b Compare December 19, 2023 03:32
@erikbern erikbern force-pushed the erikbern/decorator-lifecycle-hooks branch from 1f41c53 to 6ef7656 Compare December 19, 2023 16:30
@erikbern erikbern requested a review from aksh-at December 19, 2023 16:41
@RohanArepally
Copy link
Contributor

Looks cool! What happens if you have multiple @Enter or @EXIT? Will it run both in order or error out?

@erikbern
Copy link
Contributor Author

erikbern commented Dec 19, 2023

Looks cool! What happens if you have multiple @Enter or @EXIT? Will it run both in order or error out?

We'll run all of them, in the order they were defined! So you can have something like this

@stub.cls()
class MyClass:
    @build()
    @enter()
    def my_build(self):
        print("this one runs during image build and container start")

    @enter()
    def my_enter(self):
        print("this one runs during container start, after the previous one")

@erikbern
Copy link
Contributor Author

erikbern commented Dec 19, 2023

Running into a couple of issues when I try to use this for some slightly more complex examples. Will debug a bit and turn it into tests + fixes.

EDIT: fixed these

@erikbern
Copy link
Contributor Author

This doesn't currently support @enter and @exit when doing local function calls – we currently do that in a hacky way in cls.py

Given that this is new functionality I think it's OK to not support it for now – we can probably fix that in a later refactoring (there's some code we should share between cls and container_entrypoint)

@erikbern
Copy link
Contributor Author

Demo on some examples: modal-labs/modal-examples#507

Copy link
Contributor

@aksh-at aksh-at left a comment

Choose a reason for hiding this comment

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

This is great! Example looks much cleaner now.

method = synchronize_api(_method)
web_endpoint = synchronize_api(_web_endpoint)
asgi_app = synchronize_api(_asgi_app)
wsgi_app = synchronize_api(_wsgi_app)
build = synchronize_api(_build)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are these synchronized btw?

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 had a similar question actually. I think it has something to do with how we want _PartialFunction.__get__ to return a Function not a _Function, which means we have to synchronize the _PartialFunction class, which means we end up with internal and external versions of everything... I'm not entirely sure though.

This is obviously pretty complex and it would be nice to simplify it

@erikbern
Copy link
Contributor Author

Btw this passes all integration tests as well.

I'm pretty happy with the decorator-based lifecycle hooks SDK, but I'm a bit annoyed how complex the implementation was. A lot of this stems from complexities in the Cls class. I'll probably merge this very soon, but I think it's clear there's some opportunities to simplify our implementation of classes.

@erikbern erikbern merged commit 3293a3c into main Dec 21, 2023
17 checks passed
@erikbern erikbern deleted the erikbern/decorator-lifecycle-hooks branch December 21, 2023 04:19
@erikbern
Copy link
Contributor Author

Post-merge things I think would be nice to clean up:

  • Moving PartialFunction complexity out of functions.py into cls.py and maybe some utility stuff
  • Simplify how we import functions – try to get the function/cls from the stub instead, after importing
    • Maybe we should require the stub to be defined globally, unless the function is serialized?
  • Simplify the relationship between the user class, the Cls class, and Obj
    • Preserve the user class as the main interface, but create a new class that mixes in Cls
    • Use a separate class for classes that are dynamically looked up, rather than trying to keep both code paths together
    • Try to get rid of the Obj class
  • Unify the enter/exit stuff between Obj and _container_entrypoint.py (this is the only behavioral change – we currently don't support @enter and @exit for local calls

None of this is super urgent since it doesn't rely on the server protocol and it keeps the SDK stable, so we could potentially hold off on some of it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants