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

Pass original request headers merged with enricher response headers to Pyfunc ensembler #217

Merged
merged 9 commits into from
Jul 21, 2022

Conversation

krithika369
Copy link
Collaborator

@krithika369 krithika369 commented Jul 20, 2022

This PR makes 2 user-facing changes:

  • engines/router/missionctl/handlers/http_handler.go - Make the Enricher's response headers available to the Ensembler in the request headers. The merging of the headers was introduced in Add steps to merge original request and enricher response headers #176, intended to be used for the routing step and we hadn't propagated it down to the Ensembler. However, allowing the Ensembler access to those values would help the Ensembler make decisions based on values computed by the Enricher.
  • Pass request headers to the Pyfunc Ensembler. Previously, request headers were not available to Pyfunc ensemblers. The changes are described in detail below.

Passing Ensembler Request Headers to the Pyfunc Ensembler

To support this, the optional **kwargs argument has been added to the EnsemblerBase.ensemble method, meant to take in contextual information. This will be used by the Pyfunc Ensembler Service to propagate request headers. The main changes are below.

  • engines/pyfunc-ensembler-service/pyfunc_ensembler_runner/handler.py - Nest request headers and body under a single dict, when calling the MlFlow Pyfunc model's predict.
  • sdk/turing/ensembler.py - Pick up the headers from the model input and pass them to the ensemble() method. The signature of the ensemble method now takes in **kwargs which should be included in the custom implementation to take advantage of the headers. However, since some users may be on the older Pyfunc ensemblers, a try-except has been added to issue the call without headers if there is an associated exception. This special handling can be removed in a few releases, when users have started to include **kwargs in their implementation.

Tests

The following scenarios have been tested to be working with the new Pyfunc service as of this PR:

  • Deploying images already built using older SDK versions
  • Building and deploying images from artifacts created by older SDK versions
  • Building and deploying images from artifacts created by the new SDK version as of this PR, with or without the **kwargs

@krithika369 krithika369 marked this pull request as draft July 20, 2022 04:30
@krithika369 krithika369 marked this pull request as ready for review July 21, 2022 05:45
@krithika369 krithika369 requested a review from a team July 21, 2022 05:45
def predict(self, inputs: Dict[str, Any]) -> List[Any]:
logging.info(f"Input request payload: {inputs}")
output = self._ensembler.predict(inputs)
def predict(self, body: Dict[str, Any], headers: Dict[str, str]) -> List[Any]:
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This call's MLFlow Pyfunc model's predict which is eventually received here at runtime. The self.ensemble call from _ensemble_request finally calls the implementation provided by the user.

@@ -20,19 +20,42 @@


@pytest.mark.parametrize(
"inputs,expected", [
"ensembler_uri,inputs,headers,expected", [
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Changes to test old and new ensemblers (created with and without **kwargs)

Copy link
Collaborator

@terryyylim terryyylim left a comment

Choose a reason for hiding this comment

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

LGTM 🚀 , left some small comments!

@@ -1,4 +1,6 @@
import abc
import deprecation
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think this is being used right now, would using warnings library and raising a warning at except clause be better?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for catching, and the suggestion - removed the deprecation import and added a warning log.

logging.info(f"Input request payload: {inputs}")
output = self._ensembler.predict(inputs)
def predict(self, body: Dict[str, Any], headers: Dict[str, str]) -> List[Any]:
logging.info(f"Input request payload: {body}")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there any particular reason/benefit to log only the body but not headers (guessing it's because there might be auth details here)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Right now, the default logger uses INFO level and it's not possible to change that. So I left out the header log, to not bloat the logs further. Will capture an item in the backlog to make logging configurable.

@@ -26,7 +26,7 @@ It is not possible to select as the fallback response a route that has traffic r
{% endhint %}

## Docker
Turing will deploy specified image as a post-processor and will send the original request, responses from all routes, and the treatment configuration (if a Experiment Engine is selected, in Configure Experiment Engine), for ensembling. To configure a Docker ensembler, there are 3 sections to be filled.
Turing will deploy the specified image as a post-processor and will send the in the request payload the original request, responses from all routes, and the treatment configuration (if an Experiment Engine is selected, in Configure Experiment Engine), for ensembling. The ensembler's request headers will contain the original request headers sent to Turing, merged with the enricher's response headers (if there are duplicates, the value in the enricher's response headers will take precedence). To configure a Docker ensembler, there are 3 sections to be filled.
Copy link
Collaborator

Choose a reason for hiding this comment

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

"will send in the request payload to the original request" -> extra "the", missing "to"

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks! Rephrased for clarity.

@@ -6,6 +6,22 @@

class TestEnsembler(PyFunc):

def initialize(self, artifacts: dict):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Didn't notice this previously, but perhaps we should standardize the typings? I see different options - dict, Dict, Dict[with types].

Copy link
Collaborator Author

@krithika369 krithika369 Jul 21, 2022

Choose a reason for hiding this comment

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

Ideally yes. But there are also other inconsistencies like double vs single quote usage. We can later put in a linter to catch these everywhere and adjust styling. For now, I just changed this example of the test ensemblers to Dict to match theensemble method.

except TypeError as e:
if "got an unexpected keyword argument 'headers'" in str(e):
# This handles the legacy ensemblers
# TODO: Deprecate support for legacy ensemblers
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should perhaps specify what future SDK version would we be deprecating the legacy ensemblers.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think deprecating this depends on users moving away as well. So we need to do it at some point, but I'm not sure when. We will also need to remove some of the default_route_id related stuff. Will also track this in the backlog.

Copy link
Contributor

@deadlycoconuts deadlycoconuts left a comment

Choose a reason for hiding this comment

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

LGTM! 🚀 Thanks for cleaning up some of the stuff in the SDK files; @terryyylim also raised some interesting points which are worth thinking about so I just "+1"-ed some of them :)

@krithika369
Copy link
Collaborator Author

Thanks for the quick review, both! I'll be merging this shortly if no further comments / questions.

@krithika369 krithika369 merged commit 3ae8f23 into caraml-dev:main Jul 21, 2022
@krithika369 krithika369 deleted the pyfunc_ensembler_headers branch July 21, 2022 10:15
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.

3 participants