-
Notifications
You must be signed in to change notification settings - Fork 796
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
[Refactor] fork server processes earlier #1476
Conversation
Codecov Report
@@ Coverage Diff @@
## master #1476 +/- ##
==========================================
+ Coverage 68.65% 69.62% +0.97%
==========================================
Files 150 150
Lines 10045 10072 +27
==========================================
+ Hits 6896 7013 +117
+ Misses 3149 3059 -90
Continue to review full report at Codecov.
|
Updated |
req: #1483 |
cc @ssheng |
bentoml/entrypoint/__init__.py
Outdated
@@ -0,0 +1,133 @@ | |||
# Copyright 2019 Atalaya Tech, Inc. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you envision will be added under the /entrypoint package? What do we gain by moving to a new package?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The place before is /bentoml/server/__init__.py
. I don't think it's a proper place to wire packages including bentoml.server
itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be fine to wire one's own package. Taking a step back, we should think about how we'd like to expose our public APIs. Having a module like BentoMLController
is one choice. What are some of the best practice in Python?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Discussed offline, we will keep these in the original location under server/__init__.py
.
bentoml/entrypoint/__init__.py
Outdated
def _start_prod_server( | ||
saved_bundle_path: str, | ||
config: BentoMLConfiguration, | ||
port: Optional[int] = None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need to explicitly have port here instead of from config?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not want to change the value of config.api_server.port
. Seeing the batching app and the model app as the whole API server, it makes sense to keep config.api_server
rather than overriding it by the randomly picked port.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand the concern. Here is why we should restructure the config keys and terminology. Before we do that, I think there is an opportunity here to make the intermediate port injectable as well. First we can introduce an intermediate port (for lack of a better name).
api_server:
marshal:
intermediate_port: Null # or an actual port e.g. 6000
In the container, we can introduce a provider that first checks if there is an intermediate port defined, if not randomly reserve a port.
intermediate_port = providers.Callable(
lambda port: port if port else reserve_free_port(),
config.marshal.intermediate_port,
)
Basically, a lot of the logic here can be refactored as a provider in containers.py
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's great to have a provider for the intermediate_port
. But we have to wire after creating the new process. In your solution, the reserve_free_port
would be called twice and gave intermediate_port
different values in each process.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call that reserve_free_port
might be called twice. But it should be a solvable problem. We can use a singleton provider for creating the intermediate port. All users of the provider will get the same port. We will need to move the container creation out, however. Happy to discuss with you over a Zoom call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
UDS is superior but we might have to leave the TCP option open to make remote marshaling a possibility. We can structure the config meaningfully to reflect this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to leave the TCP option open to make remote marshaling a possibility. We can structure the config meaningfully to reflect this.
api_server:
port: 5000
enable_microbatch: False
run_with_ngrok: False
enable_swagger: True
enable_metrics: True
enable_feedback: True
max_request_size: 20971520
workers: 1
timeout: 60
model_server:
port: Null # default: api_server.port when enable_marshal=False
marshal_server:
port: Null # default: api_server.port when enable_marshal=True
like this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm thinking something like the following where the user can choose the connector type of provide related configs. Basically the schema for connector is a union of UDS and TCP connector schemas.
If UDS is chosen,
api_server:
marshal:
connector:
type: UDS
uds_related_key1: value1
uds_related_key2: value2
Or, if TCP is chosen,
api_server:
marshal:
connector:
type: TCP
port: None # or some configured port
tcp_related_key1: value1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For UDS support, I'd like to have a simple host
field in URI format, just like gunicorn and Nginx does.
127.0.0.1:5000
unix:/tmp/gunicorn.sock
Your schema is fancy but looks too powerful for me.
I think you can draft a new PR to demonstrate your idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After all, the config structure reflects our system design. We can continue the discussion in the new PR/issue.
bentoml/entrypoint/__init__.py
Outdated
|
||
def _start_prod_batching_server( | ||
saved_bundle_path: str, | ||
api_server_port: int, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same question here. Why do we need to explicitly have api_server_port here instead of from config?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above.
@@ -178,6 +180,7 @@ def __init__( | |||
"or launch more microbatch instances to accept more concurrent connection.", | |||
self.CONNECTION_LIMIT, | |||
) | |||
self._client = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We probably don't need to lazy initialize client here. Client is pretty much always needed, correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Client is pretty much always needed, correct?
Yeah. But IMO we best only do value assignment operations in the __init__
.
In addition, in this case, an aiohttp client session should be initialized with a running asyncio event loop.
aio-libs/aiohttp#3331
if self._client is None: | ||
jar = aiohttp.DummyCookieJar() | ||
if self.outbound_unix_socket: | ||
conn = aiohttp.UnixConnector(path=self.outbound_unix_socket,) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for UDS.
bento_service, port=api_server_port, enable_swagger=enable_swagger | ||
) | ||
api_server.start() | ||
api_server = BentoAPIServer(bento_service, enable_swagger=enable_swagger) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think start_dev_server
could follow the same pattern as start_prod_server
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. But that should be done by another PR. It involves more than the prod server.
@@ -183,14 +181,18 @@ def __init__( | |||
|
|||
self.setup_routes() | |||
|
|||
def start(self): | |||
def start(self, port: int, host: str = "127.0.0.1"): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can host be in the config?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. But it should be done in another PR.
|
||
import psutil | ||
from dependency_injector.wiring import Provide as P |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a big issue. I think it's more readable to fully spell it out. Same for container.
* [Refactor] earlier fork * [Refactor] Add server entrypoint module * format code * clean * move configuration to entrypoint * clean * revert changes about container * Move start_prod_server * make pylint happy * fix wiring
Description
requires test utils in #1467 #1347
Fork before importing third-party dependencies to avoid hacking like #925
Should introduce a lib of process management in the future. Now, this function was provided by Gunicorn.
Motivation and Context
How Has This Been Tested?
Types of changes
Component(s) if applicable
Checklist:
./dev/format.sh
and./dev/lint.sh
script have passed(instructions).