Skip to content

Commit

Permalink
Merge pull request #236 from isi-nlp/130-restapi
Browse files Browse the repository at this point in the history
Add REST API around decoder
  • Loading branch information
thammegowda authored Aug 3, 2020
2 parents 324379d + 20d3a93 commit 19c56bb
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 2 deletions.
11 changes: 11 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
ARG experiment_dir
FROM python:3.7
# RUN echo ${experiment_dir}
# RUN echo "hello world"
# COPY ${experiment_dir} /experiment/
COPY ./experiments/sample-exp/ /experiment/
COPY . /rtg
WORKDIR /rtg
RUN pip install -e ./
RUN python -m rtg.pipeline /experiment/
CMD python -m rtg.deploy /experiment/
3 changes: 2 additions & 1 deletion docs/clitools.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ The following command line tools are added when `rtg` is installed using pip.
| Command | Purpose
| rtg-pipe | Run rtg-prep, rtg-train and test case evaluation
| rtg-decode | Decode new source files using the values set in `conf.yml`
| rtg-decode-pro | Decode new source files using the values that you supply from CLI args
| rtg-export | Export an experiment
| rtg-fork | Fork an experiment with/without same conf, code, data, vocabularies etc
| rtg-serve | Serve an RTG model over HTTP API using Flask server
| rtg-decode-pro | Decode new source files using the values that you supply from CLI args
| rtg-prep | Prepare an experiment. You should be using `rtg-pipe`
| rtg-train | Train a model. You should be using `rtg-pipe`
| rtg-syscomb | System combination. Dont bother about it for now.
Expand Down
2 changes: 2 additions & 0 deletions docs/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@ include::clitools.adoc[]

include::environ.adoc[]

include::serve.adoc[]

include::develop.adoc[]

66 changes: 66 additions & 0 deletions docs/serve.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@

== RTG Serve

RTG model can be served using Flask Server.


[source,commandline]
----
$ python -m rtg.serve -h # rtg-serve
[07-13 22:38:01] p49095 {__init__:53} INFO - rtg v0.3.1 from /Users/tg/work/me/rtg
usage: rtg.serve [-h] [-sc] [-p PORT] [-ho HOST] [-msl MAX_SRC_LEN] exp_dir
deploy a model to a RESTful react server
positional arguments:
exp_dir Experiment directory
optional arguments:
-h, --help show this help message and exit
-sc, --skip-check Skip Checking whether the experiment dir is prepared
and trained (default: False)
-p PORT, --port PORT port to run server on (default: 6060)
-ho HOST, --host HOST
Host address to bind. (default: 0.0.0.0)
-msl MAX_SRC_LEN, --max-src-len MAX_SRC_LEN
max source len; longer seqs will be truncated
(default: None)
----


To launch a service for `runs/001-tfm` experiment

`python -m rtg.serve -sc runs/001-tfm`

It prints :
`* Running on http://0.0.0.0:6060/ (Press CTRL+C to quit)`

Currently only `/translate` API is supported. It accepts both `GET` with query params and `POST` with form params.

NOTE: batch decoding is yet to be supported. Current decoder decodes only one sentence at a time.

An example POST request:
----
curl --data "source=Comment allez-vous?" --data "source=Bonne journée" http://localhost:6060/translate
----
[source,json]
----
{
"source": [
"Comment allez-vous?",
"Bonne journée"
],
"translation": [
"How are you?",
"Have a nice day"
]
}
----
You can also request like GET method as `http://localhost:6060/translate?source=text1&source=text2`
after properly URL encoding the `text1` `text2`. This should only be used for quick testing in your web browser.




2 changes: 1 addition & 1 deletion rtg/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ def run(self, run_tests=True):


def parse_args():
parser = argparse.ArgumentParser(prog="rtg.prep", description="prepare NMT experiment")
parser = argparse.ArgumentParser(prog="rtg-pipe", description="RTG Pipeline CLI")
parser.add_argument("exp", metavar='EXP_DIR', help="Working directory of experiment", type=Path)
parser.add_argument("conf", metavar='conf.yml', type=Path, nargs='?',
help="Config File. By default <work_dir>/conf.yml is used")
Expand Down
82 changes: 82 additions & 0 deletions rtg/serve.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env python
"""
Serves an RTG model using Flask HTTP server
"""
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter

from flask import Flask, request, jsonify
import torch

from rtg import TranslationExperiment as Experiment, log
from rtg.module.decoder import Decoder


def prepare_decoder(cli_args):
# No grads required for decode
torch.set_grad_enabled(False)
exp = Experiment(cli_args.pop("exp_dir"), read_only=True)
dec_args = exp.config.get("decoder") or exp.config["tester"].get("decoder", {})
validate_args(cli_args, dec_args, exp)
decoder = Decoder.new(exp, ensemble=dec_args.pop("ensemble", 1))
return decoder, dec_args


def attach_translate_route(app, decoder, dec_args):

app.config['JSON_AS_ASCII'] = False

@app.route("/translate", methods=["POST", "GET"])
def translate():
if request.method not in ("POST", "GET"):
return "GET and POST are supported", 400
if request.method == 'GET':
sources = request.args.getlist("source", None)
else:
sources = request.form.getlist("source", None)
if not sources:
return "Please provide parameter 'source'", 400

translations = []
for source in sources:
translated = decoder.decode_sentence(source, **dec_args)[0][1]
translations.append(translated)
res = dict(source=sources, translation=translations)
return jsonify(res)


def validate_args(cli_args, conf_args, exp: Experiment):
if not cli_args.pop("skip_check"): # if --skip-check is not requested
assert exp.has_prepared(), (f'Experiment dir {exp.work_dir} is not ready to train.'
f' Please run "prep" sub task')
assert exp.has_trained(), (f"Experiment dir {exp.work_dir} is not ready to decode."
f" Please run 'train' sub task or --skip-check to ignore this")

def parse_args():
parser = ArgumentParser(
prog="rtg.serve",
description="deploy a model to a RESTful react server",
formatter_class=ArgumentDefaultsHelpFormatter,
)
parser.add_argument("exp_dir", help="Experiment directory", type=str)
parser.add_argument("-sc", "--skip-check", action="store_true",
help="Skip Checking whether the experiment dir is prepared and trained")
parser.add_argument("-p", "--port", type=int, help="port to run server on", default=6060)
parser.add_argument("-ho", "--host", help="Host address to bind.", default='0.0.0.0')
parser.add_argument("-msl", "--max-src-len", type=int,
help="max source len; longer seqs will be truncated")
args = vars(parser.parse_args())
return args


def main():
cli_args = parse_args()
decoder, dec_args = prepare_decoder(cli_args)
app = Flask(__name__)
#CORS(app) # TODO: insecure
app.debug = True
attach_translate_route(app, decoder, dec_args)
app.run(port=cli_args["port"], host=cli_args["host"])


if __name__ == "__main__":
main()
4 changes: 4 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@
'nlcodec >= 0.2.2',
'torch >= 1.4'
],
extra_requires={
'serve': ['flask >= 1.1.2'],
},
python_requires='>=3.7',
entry_points={
'console_scripts': [
Expand All @@ -54,6 +57,7 @@
'rtg-prep=rtg.prep:main',
'rtg-train=rtg.train:main',
'rtg-fork=rtg.fork:main',
'rtg-serve=rtg.serve:main',
'rtg-syscomb=rtg.syscomb.__main__:main',
],
}
Expand Down

0 comments on commit 19c56bb

Please sign in to comment.