Invoking sagemaker endpoint with CustomAttributes - amazon-web-services

I am trying to invoke my SageMaker endpoint and pass CustomAttributes argument specified here.
what I want to know is how to retrieve the CustomAttributes in the model endpoint?
I created an inference.py file for my endpoint that has the following structure:
imports
def get_device():
device = 'cuda:0' if torch.cuda.is_available() else 'cpu'
return device
def model_fn(model_dir):
return model
def transform_fn(model, request_body, content_type, accept)
return json.dumps(predictions)
just as request_body, content_type, accept are passed to transform_fn, i want to pass the CustomAttributes. Is this possible and if so how can i do it ?
Thanks in advance!

It is convenient to structure the inference code by having full control of all the underlying steps.
As the documentation "Adapting Your Own Inference Container" suggests, you can arrange 4 functions: model_fn, input_fn, predict_fn and output_fn.
Beyond these, you can create your own handler to handle all the attributes you pass when you invoke an endpoint ("How to implement the pre- and/or post-processing handler(s)").
You can have the input_handler / output_handler pair of functions or a single handler function.
Below is an example of code for a generic inference script.
Inside your block of endpoint's invocation:
import boto3
import json
runtime = boto3.Session().client('sagemaker-runtime')
runtime_client.invoke_endpoint(
EndpointName = your_endpoint_name,
Body = your_data,
CustomAttributes = json.dumps(your_attributes_dict),
ContentType = your_content_type
)
and inside your inference.py:
import json
def handler(data, context):
processed_input = _process_input(data, context)
custom_attrs = json.loads(context.custom_attributes)
# here place your function to parse and use your custom_attrs json
response = requests.post(context.rest_uri, data=processed_input)
return _process_output(response, context)
def _process_input(data, context):
# your _process_input to decode the request_content_type
if context.request_content_type == YOUR_CONTEXT_TYPE:
return your_process_func(data)
raise ValueError('{{"error": "unsupported content type {}"}}'.format(
context.request_content_type or "unknown"))
def _process_output(data, context):
if data.status_code != 200:
raise ValueError(data.content.decode('utf-8'))
response_content_type = context.accept_header
prediction = data.content
return prediction, response_content_type
Pay attention to this note:
Note that if handler function is implemented, input_handler and
output_handler are ignored.
It means that if you use a framework like TensorFlow or PyTorch, you will have to see how to override these methods starting from their default handlers.

Related

Call another Lambda and pass through parameters a function that returns a set of information

I'm currently developing a lambda that invokes another Lambda with Boto3. However, I need from one statement to retrieve a group of results and send them through the invoke payload to the other Lambda. However, I can't find how to send this function as a parameter to call another Lambda and pass through parameters a function that returns a set of information.
I have implemented this method:
from MysqlConnection import MysqlConnection
from sqlalchemy import text
def make_dataframe(self):
conn = MysqlConnection()
query = text("""select * from queue WHERE estatus = 'PENDING' limit 4;""")
df = pd.read_sql_query(query,conn.get_engine())
return df.to_json()
This is the Lambda handler:
import json
import boto3
from MysqlConnection import MysqlConnection
from Test import Test
client = boto3.client('lambda')
def lambda_handler(event, context):
mydb = MysqlConnection()
print(mydb.get_engine)
df = Test()
df.make_dataframe()
object = json.loads(df.make_dataframe())
response = client.invoke(
FunctionName='arn:aws:lambda:',
InvocationType='RequestResponse'#event
Payload=json.dumps(object)
)
responseJson = json.load(response['Payload'])
print('\n')
print(responseJson)
print('\n')
What you're doing is correct in terms of structuring your call.
I assume the problem is with your payload structure and whether its stringified.
I would try invoke your lambda with an empty payload and see what happens. If it works with empty payload then its your payload serialising, if it doesnt work with empty payload then its something else.
In cloudwatch what do your logs of both your "runner" lambda and your "target" lambda say?
It might also be a permissions thing - you will need to specify and grant execute permissions on your runner lambda.
after days of refactoring and research I am sharing the answer. It is about packing the json.dump object and inside the handler place the method with the response already packed
This a method to parent child
class Test:
def make_dataframe(self):
conn = MysqlConnection()
query = text("""select * from TEST WHERE status'PEN' limit 4;""")
df = pd.read_sql_query(query,conn.get_engine())
lst = df.values.tolist()
obj = json.dumps(lst, cls=CustomJSONEncoder)
return obj
def lambda_handler(event, context):
mydb = MysqlConnection()
df = Test()
response = client.invoke(
FunctionName='arn:aws:lambda:',
InvocationType='RequestResponse',
Payload= df.make_dataframe()
)
responseJson = json.load(response['Payload'])
print('\n')
print(responseJson)
print('\n')
`

Calling a Django function inside an AWS Lambda

I want to defer some processing load from my Django app to an AWS Lambda.
I'm calling my code from the Lambda like this:
lambda.py:
#bc_lambda(level=logging.INFO, service=LAMBDA_SERVICE)
def task_handler(event, context):
message = event["Records"][0]["body"]
renderer = get_renderer_for(message)
result = renderer.render()
return result
get_renderer_for is a factory method that returns an instance of the class Renderer:
from myproject.apps.engine.documents import (
DocumentsLoader,
SourceNotFound,
source_from_version,
)
from myproject.apps.engine.environment import Environment
class Renderer:
def __init__(self, message):
self.message = message
def render(self):
ENVIRONMENT = Environment(DocumentsLoader())
version_id = self.message.get("version_id")
try:
source = source_from_version(version_id)
except SourceNotFound:
source = None
template = ENVIRONMENT.from_string(source)
if template:
return template.render(self.message)
return None
def get_renderer_for(message):
"""
Factory method that returns an instance of the Renderer class
"""
return Renderer(message)
In CloudWatch, I see I'm getting this error: module initialization error. Apps aren't loaded yet.
I understand that Django is not available for the Lambda function, right? How can I fix this? How can I make the rest of the project available to the lambda function?
The only two libraries that Lambda supports out of the box are the standard library and boto3.
There are several ways to install external Python libraries for use in Lambda. I recommend uploading them as a Lambda layer. This is a good guide: https://medium.com/#qtangs/creating-new-aws-lambda-layer-for-python-pandas-library-348b126e9f3e

google-ml-engine custom prediction routine error responses

I have a custom prediction routine in google-ml-engine. Works very well.
I now am doing input checking on the instance data, and want to return error responses from my predict routine.
The example: https://cloud.google.com/ai-platform/prediction/docs/custom-prediction-routines
Raises exceptions on input errors, etc. However, when this happens the response body always has {'error': Prediction failed: unknown error}. I can see the correct errors are being logged in google cloud console, but the https response is always the same unknown error.
My question is:
How to make the Custom prediction routine return a proper error code and error message string?
Instead of returning a prediction, I can return an error string/code in prediction -but it ends up in the prediction part of the response which seems hacky and doesn't get any of the google errors eg based on instance size.
root:test_deployment.py:35 {'predictions': {'error': "('Instance does not include required sensors', 'occurred at index 0')"}}
What's the best way to do this?
Thanks!
David
Please take a look at the following code, I created a _validate function inside predict and use a custom Exception class.
Basically, I validate instances, before I call the model predict method and handle the exception.
There may be some overhead to the response time when doing this validation, which you need to test for your use case.
requests = [
"god this episode sucks",
"meh, I kinda like it",
"what were the writer thinking, omg!",
"omg! what a twist, who would'v though :o!",
99999
]
api = discovery.build('ml', 'v1')
parent = 'projects/{}/models/{}/versions/{}'.format(PROJECT, MODEL_NAME, VERSION_NAME)
parent = 'projects/{}/models/{}'.format(PROJECT, MODEL_NAME)
response = api.projects().predict(body=request_data, name=parent).execute()
{'predictions': [{'Error code': 1, 'Message': 'Invalid instance type'}]}
Custom Prediction class:
import os
import pickle
import numpy as np
import logging
from datetime import date
import tensorflow.keras as keras
class CustomModelPredictionError(Exception):
def __init__(self, code, message='Error found'):
self.code = code
self.message = message # you could add more args
def __str__(self):
return str(self.message)
def isstr(s):
return isinstance(s, str) or isinstance(s, bytes)
def _validate(instances):
for instance in instances:
if not isstr(instance):
raise CustomModelPredictionError(1, 'Invalid instance type')
return instances
class CustomModelPrediction(object):
def __init__(self, model, processor):
self._model = model
self._processor = processor
def _postprocess(self, predictions):
labels = ['negative', 'positive']
return [
{
"label":labels[int(np.round(prediction))],
"score":float(np.round(prediction, 4))
} for prediction in predictions]
def predict(self, instances, **kwargs):
try:
instances = _validate(instances)
except CustomModelPredictionError as c:
return [{"Error code": c.code, "Message": c.message}]
else:
preprocessed_data = self._processor.transform(instances)
predictions = self._model.predict(preprocessed_data)
labels = self._postprocess(predictions)
return labels
#classmethod
def from_path(cls, model_dir):
model = keras.models.load_model(
os.path.join(model_dir,'keras_saved_model.h5'))
with open(os.path.join(model_dir, 'processor_state.pkl'), 'rb') as f:
processor = pickle.load(f)
return cls(model, processor)
Complete code in this notebook.
If it is still relevant to you, I found a way by using google internal libraries (not sure if it would be endorsed by Google though).
AI platform custom prediction wrapping code only returns custom error message if the Exception thrown is a specific one from their internal library.
It might also not be super reliable as you would have very little control in case Google wants to change it.
class Predictor(object):
def predict(self, instances, **kwargs):
# Your prediction code here
# This is an internal google library, it should be available at prediction time.
from google.cloud.ml.prediction import prediction_utils
raise prediction_utils.PredictionError(0, "Custom error message goes here")
#classmethod
def from_path(cls, model_dir):
# Your logic to load the model here
You would get the following message in your HTTP response
Prediction failed: Custom error message goes here

Can I pass arguments to a function in monkeypatch.setattr for a function used multiple times in one view?

My web application makes API calls to Spotify. In one of my Flask views I use the same method with different endpoints. Specifically:
sh = SpotifyHelper()
...
#bp.route('/profile', methods=['GET', 'POST'])
#login_required
def profile():
...
profile = sh.get_data(header, 'profile_endpoint')
...
playlist = sh.get_data(header, 'playlist_endpoint')
...
# There are 3 more like this to different endpoints -- history, top_artists, top_tracks
...
return render_template(
'profile.html',
playlists=playlists['items'],
history=history['items'],
...
)
I do not want to make an API call during testing so I wrote a mock.json that replaces the JSON response from the API. I have done this successfully when the method is only used once per view:
class MockResponse:
#staticmethod
def profile_response():
with open(path + '/music_app/static/JSON/mock.json') as f:
response = json.load(f)
return response
#pytest.fixture
def mock_profile(monkeypatch):
def mock_json(*args, **kwargs):
return MockResponse.profile_response()
monkeypatch.setattr(sh, "get_data", mock_json)
My problem is that I need to call get_data to different endpoints with different responses. My mock.json is written:
{'playlists': {'items': [# List of playlist data]},
'history': {'items': [# List of playlist data]},
...
So for each API endpoint I need something like
playlists = mock_json['playlists']
history = mock_json['history']
I can write mock_playlists(), mock_history(), etc., but how do I write a monkeypatch for each? Is there some way to pass the endpoint argument to monkeypatch.setattr(sh, "get_data", mock_???)?
from unittest.mock import MagicMock
#other code...
mocked_response = MagicMock(side_effect=[
# write it in the order of calls you need
profile_responce_1, profile_response_2 ... profile_response_n
])
monkeypatch.setattr(sh, "get_data", mocked_response)

json serialisation of dates on flask restful

I have the following resource:
class Image(Resource):
def get(self, db_name, col_name, image_id):
col = mongo_client[db_name][col_name]
image = col.find_one({'_id':ObjectId(image_id)})
try:
image['_id'] = str(image['_id'])
except TypeError:
return {'image': 'notFound'}
return {'image':image}
linked to a certain endpoint.
However, image contains certain datetime objects inside. I could wrap this around with `json.dumps(..., default=str), but I see that there is a way of enforcing this on flask-restful. It's just not clear to me what exactly needs to be done.
In particular, I read:
It is possible to configure how the default Flask-RESTful JSON
representation will format JSON by providing a RESTFUL_JSON
attribute on the application configuration.
This setting is a dictionary with keys that
correspond to the keyword arguments of json.dumps().
class MyConfig(object):
RESTFUL_JSON = {'separators': (', ', ': '),
'indent': 2,
'cls': MyCustomEncoder}
But it's not clear to me where exactly this needs to be placed. Tried a few things and it didn't work.
EDIT:
I finally solved with this:
Right after
api = Api(app)
I added:
class CustomEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime.datetime):
#return int(obj.strftime('%s'))
return str(obj)
elif isinstance(obj, datetime.date):
#return int(obj.strftime('%s'))
return str(obj)
return json.JSONEncoder.default(self, obj)
def custom_json_output(data, code, headers=None):
dumped = json.dumps(data, cls=CustomEncoder)
resp = make_response(dumped, code)
resp.headers.extend(headers or {})
return resp
api = Api(app)
api.representations.update({
'application/json': custom_json_output
})
Just cleared this out, you just have to do the following:
app = Flask(__name__)
api = Api(app)
app.config['RESTFUL_JSON'] = {'cls':MyCustomEncoder}
This works both for plain Flask and Flask-RESTful.
NOTES:
1) Apparently the following part of the documentation is not that clear:
It is possible to configure how the default Flask-RESTful JSON
representation will format JSON by providing a RESTFUL_JSON attribute
on the application configuration. This setting is a dictionary with
keys that correspond to the keyword arguments of json.dumps().
class MyConfig(object):
RESTFUL_JSON = {'separators': (', ', ': '),
'indent': 2,
'cls': MyCustomEncoder}
2)Apart from the 'cls' argument you can actually overwrite any keyword argument of the json.dumps function.
Having created a Flask app, e.g. like so:
root_app = Flask(__name__)
place MyConfig in some module e.g. config.py and then configure root_app like:
root_app.config.from_object('config.MyConfig')