From c1b280231304bd2af4b5dba800299105330e7360 Mon Sep 17 00:00:00 2001 From: Sanyam Kapoor Date: Thu, 28 Jun 2018 20:37:21 -0700 Subject: [PATCH] Add new TF-Serving component with sample task (#152) * Add new TF-Serving component with sample task * Unify nmslib and t2t packages, need to be cohesive * [WIP] update references to the package * Replace old T2T problem * Add representative code for encoding/decoding from tf serving service * Add rest API port to TF serving (replaces custom http proxy) * Fix linting * Add NMSLib creator and server components * Add docs to CLI module --- code_search/README.md | 4 +- code_search/{language_task => app}/Dockerfile | 29 +- .../code_search}/__init__.py | 0 .../app/code_search/nmslib/__init__.py | 0 code_search/app/code_search/nmslib/cli.py | 90 ++++ .../code_search/nmslib}/gcs.py | 0 .../app/code_search/nmslib/search_engine.py | 69 ++++ .../code_search/nmslib}/search_server.py | 5 +- code_search/app/code_search/t2t/__init__.py | 1 + code_search/app/code_search/t2t/query.py | 32 ++ .../t2t}/similarity_transformer.py | 0 code_search/app/requirements.txt | 8 + code_search/{indexing_server => app}/setup.py | 10 +- .../{language_task => app}/t2t-entrypoint.sh | 0 .../{language_task => }/build_image.sh | 8 +- code_search/indexing_server/Dockerfile | 9 - code_search/indexing_server/build_image.sh | 21 - .../indexing_server/nmslib_flask/cli.py | 71 ---- .../nmslib_flask/search_engine.py | 46 --- code_search/indexing_server/requirements.txt | 4 - code_search/kubeflow/app.yaml | 6 + .../kubeflow/components/nms-creator.jsonnet | 7 + .../kubeflow/components/nms-server.jsonnet | 7 + code_search/kubeflow/components/nms.libsonnet | 119 ++++++ .../kubeflow/components/params.libsonnet | 29 ++ .../components/t2t-translate-serving.jsonnet | 21 + .../vendor/kubeflow/tf-serving/README.md | 73 ++++ .../vendor/kubeflow/tf-serving/parts.yaml | 35 ++ .../tf-serving-all-features.jsonnet | 24 ++ .../kubeflow/tf-serving/tf-serving.libsonnet | 387 ++++++++++++++++++ .../vendor/kubeflow/tf-serving/util.libsonnet | 25 ++ code_search/language_task/requirements.txt | 2 - .../language_task/t2t_problems/__init__.py | 2 - 33 files changed, 964 insertions(+), 180 deletions(-) rename code_search/{language_task => app}/Dockerfile (50%) rename code_search/{indexing_server/nmslib_flask => app/code_search}/__init__.py (100%) create mode 100644 code_search/app/code_search/nmslib/__init__.py create mode 100644 code_search/app/code_search/nmslib/cli.py rename code_search/{indexing_server/nmslib_flask => app/code_search/nmslib}/gcs.py (100%) create mode 100644 code_search/app/code_search/nmslib/search_engine.py rename code_search/{indexing_server/nmslib_flask => app/code_search/nmslib}/search_server.py (80%) create mode 100644 code_search/app/code_search/t2t/__init__.py create mode 100644 code_search/app/code_search/t2t/query.py rename code_search/{language_task/t2t_problems => app/code_search/t2t}/similarity_transformer.py (100%) create mode 100644 code_search/app/requirements.txt rename code_search/{indexing_server => app}/setup.py (67%) rename code_search/{language_task => app}/t2t-entrypoint.sh (100%) rename code_search/{language_task => }/build_image.sh (80%) delete mode 100644 code_search/indexing_server/Dockerfile delete mode 100755 code_search/indexing_server/build_image.sh delete mode 100644 code_search/indexing_server/nmslib_flask/cli.py delete mode 100644 code_search/indexing_server/nmslib_flask/search_engine.py delete mode 100644 code_search/indexing_server/requirements.txt create mode 100644 code_search/kubeflow/components/nms-creator.jsonnet create mode 100644 code_search/kubeflow/components/nms-server.jsonnet create mode 100644 code_search/kubeflow/components/nms.libsonnet create mode 100644 code_search/kubeflow/components/t2t-translate-serving.jsonnet create mode 100644 code_search/kubeflow/vendor/kubeflow/tf-serving/README.md create mode 100644 code_search/kubeflow/vendor/kubeflow/tf-serving/parts.yaml create mode 100644 code_search/kubeflow/vendor/kubeflow/tf-serving/prototypes/tf-serving-all-features.jsonnet create mode 100644 code_search/kubeflow/vendor/kubeflow/tf-serving/tf-serving.libsonnet create mode 100644 code_search/kubeflow/vendor/kubeflow/tf-serving/util.libsonnet delete mode 100644 code_search/language_task/requirements.txt delete mode 100644 code_search/language_task/t2t_problems/__init__.py diff --git a/code_search/README.md b/code_search/README.md index 7644af3c..9e4ec64e 100644 --- a/code_search/README.md +++ b/code_search/README.md @@ -86,11 +86,11 @@ $ gcloud auth configure-docker * Build and push the image ``` -$ PROJECT=my-project ./language_task/build_image.sh +$ PROJECT=my-project ./build_image.sh ``` and a GPU image ``` -$ GPU=1 PROJECT=my-project ./language_task/build_image.sh +$ GPU=1 PROJECT=my-project ./build_image.sh ``` See [GCR Pushing and Pulling Images](https://cloud.google.com/container-registry/docs/pushing-and-pulling) for more. diff --git a/code_search/language_task/Dockerfile b/code_search/app/Dockerfile similarity index 50% rename from code_search/language_task/Dockerfile rename to code_search/app/Dockerfile index 1dc1900d..4446372b 100644 --- a/code_search/language_task/Dockerfile +++ b/code_search/app/Dockerfile @@ -1,28 +1,27 @@ -# NOTE: The context for this build must be the `language_task` directory +# NOTE: The context for this build must be the `app` directory ARG BASE_IMAGE_TAG=1.8.0-py3 FROM tensorflow/tensorflow:$BASE_IMAGE_TAG -ADD requirements.txt / +ADD . /app -RUN pip3 --no-cache-dir install -r /requirements.txt &&\ +WORKDIR /app + +ENV T2T_USR_DIR=/app/code_search/t2t + +RUN pip3 --no-cache-dir install . &&\ apt-get update && apt-get install -y jq &&\ - rm -rf /var/lib/apt/lists/* - -VOLUME ["/data", "/output"] - -ADD t2t_problems/* /t2t_problems/ -ADD t2t-entrypoint.sh /usr/local/sbin/t2t-entrypoint - -ENV T2T_USR_DIR=/t2t_problems - -WORKDIR /t2t_problems - -#ENTRYPOINT ["/usr/local/sbin/t2t-entrypoint"] + rm -rf /var/lib/apt/lists/* &&\ + ln -s /app/t2t-entrypoint.sh /usr/local/sbin/t2t-entrypoint # TODO(sanyamkapoor): A workaround for tensorflow/tensor2tensor#879 RUN apt-get update && apt-get install -y curl python &&\ curl https://sdk.cloud.google.com | bash &&\ rm -rf /var/lib/apt/lists/* + +VOLUME ["/data", "/output"] + +EXPOSE 8008 + ENTRYPOINT ["bash"] diff --git a/code_search/indexing_server/nmslib_flask/__init__.py b/code_search/app/code_search/__init__.py similarity index 100% rename from code_search/indexing_server/nmslib_flask/__init__.py rename to code_search/app/code_search/__init__.py diff --git a/code_search/app/code_search/nmslib/__init__.py b/code_search/app/code_search/nmslib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/code_search/app/code_search/nmslib/cli.py b/code_search/app/code_search/nmslib/cli.py new file mode 100644 index 00000000..c9b8df01 --- /dev/null +++ b/code_search/app/code_search/nmslib/cli.py @@ -0,0 +1,90 @@ +""" +This module serves as the entrypoint to either create an nmslib index or +start a Flask server to serve the index via a simple REST interface. It +internally talks to TF Serving for inference related tasks. The +two entrypoints `server` and `creator` are exposed as `nmslib-create` +and `nmslib-serve` binaries (see `setup.py`). Use `-h` to get a list +of input CLI arguments to both. +""" + +import sys +import os +import argparse +import numpy as np + +from code_search.nmslib.gcs import maybe_download_gcs_file, maybe_upload_gcs_file +from code_search.nmslib.search_engine import CodeSearchEngine +from code_search.nmslib.search_server import CodeSearchServer + +def parse_server_args(args): + parser = argparse.ArgumentParser(prog='nmslib Flask Server') + + parser.add_argument('--tmp-dir', type=str, metavar='', default='/tmp/nmslib', + help='Path to temporary data directory') + parser.add_argument('--index-file', type=str, required=True, + help='Path to index file created by nmslib') + parser.add_argument('--problem', type=str, required=True, + help='Name of the T2T problem') + parser.add_argument('--data-dir', type=str, metavar='', default='/tmp', + help='Path to working data directory') + parser.add_argument('--serving-url', type=str, required=True, + help='Complete URL to TF Serving Inference server') + parser.add_argument('--host', type=str, metavar='', default='0.0.0.0', + help='Host to start server on') + parser.add_argument('--port', type=int, metavar='', default=8008, + help='Port to bind server to') + + args = parser.parse_args(args) + args.tmp_dir = os.path.expanduser(args.tmp_dir) + args.index_file = os.path.expanduser(args.index_file) + args.data_dir = os.path.expanduser(args.data_dir) + + return args + + +def parse_creator_args(args): + parser = argparse.ArgumentParser(prog='nmslib Index Creator') + + parser.add_argument('--data-file', type=str, required=True, + help='Path to csv data file for human-readable data') + parser.add_argument('--index-file', type=str, metavar='', default='/tmp/index.nmslib', + help='Path to output index file') + parser.add_argument('--tmp-dir', type=str, metavar='', default='/tmp/nmslib', + help='Path to temporary data directory') + + return parser.parse_args(args) + +def server(): + args = parse_server_args(sys.argv[1:]) + + if not os.path.isdir(args.tmp_dir): + os.makedirs(args.tmp_dir, exist_ok=True) + + # Download relevant files if needed + index_file = maybe_download_gcs_file(args.index_file, args.tmp_dir) + + search_engine = CodeSearchEngine(args.problem, args.data_dir, args.serving_url, + index_file) + + search_server = CodeSearchServer(engine=search_engine, + host=args.host, port=args.port) + search_server.run() + + +def creator(): + args = parse_creator_args(sys.argv[1:]) + + if not os.path.isdir(args.tmp_dir): + os.makedirs(args.tmp_dir, exist_ok=True) + + data_file = maybe_download_gcs_file(args.data_file, args.tmp_dir) + + # TODO(sanyamkapoor): parse data file into a numpy array + + data = np.load(data_file) + + tmp_index_file = os.path.join(args.tmp_dir, os.path.basename(args.index_file)) + + CodeSearchEngine.create_index(data, tmp_index_file) + + maybe_upload_gcs_file(tmp_index_file, args.index_file) diff --git a/code_search/indexing_server/nmslib_flask/gcs.py b/code_search/app/code_search/nmslib/gcs.py similarity index 100% rename from code_search/indexing_server/nmslib_flask/gcs.py rename to code_search/app/code_search/nmslib/gcs.py diff --git a/code_search/app/code_search/nmslib/search_engine.py b/code_search/app/code_search/nmslib/search_engine.py new file mode 100644 index 00000000..06e26c43 --- /dev/null +++ b/code_search/app/code_search/nmslib/search_engine.py @@ -0,0 +1,69 @@ +import json +import requests +import nmslib +import numpy as np +from tensor2tensor import problems # pylint: disable=unused-import +from code_search.t2t.query import get_encoder_decoder, encode_query + + +class CodeSearchEngine: + """This is a utility class which takes an nmslib + index file and a data file to return data from""" + def __init__(self, problem: str, data_dir: str, serving_url: str, + index_file: str): + self._serving_url = serving_url + self._problem = problem + self._data_dir = data_dir + self._index_file = index_file + + self.index = CodeSearchEngine.nmslib_init() + self.index.loadIndex(index_file) + + def embed(self, query_str): + """This function gets the vector embedding from + the target inference server. The steps involved are + encoding the input query and decoding the responses + from the TF Serving service + TODO(sanyamkapoor): This code is still under construction + and only representative of the steps needed to build the + embedding + """ + encoder, decoder = get_encoder_decoder(self._problem, self._data_dir) + encoded_query = encode_query(encoder, query_str) + data = {"instances": [{"input": {"b64": encoded_query}}]} + + response = requests.post(url=self._serving_url, + headers={'content-type': 'application/json'}, + data=json.dumps(data)) + + result = response.json() + for prediction in result['predictions']: + prediction['outputs'] = decoder.decode(prediction['outputs']) + + return result['predicts'][0]['outputs'] + + def query(self, query_str: str, k=2): + embedding = self.embed(query_str) + idxs, dists = self.index.knnQuery(embedding, k=k) + + # TODO(sanyamkapoor): initialize data map and return + # list of dicts + # [ + # {'src': self.data_map[idx], 'dist': dist} + # for idx, dist in zip(idxs, dists) + # ] + return idxs, dists + + @staticmethod + def nmslib_init(): + """Initializes an nmslib index object""" + index = nmslib.init(method='hnsw', space='cosinesimil') + return index + + @staticmethod + def create_index(data: np.array, save_path: str): + """Add numpy data to the index and save to path""" + index = CodeSearchEngine.nmslib_init() + index.addDataPointBatch(data) + index.createIndex({'post': 2}, print_progress=True) + index.saveIndex(save_path) diff --git a/code_search/indexing_server/nmslib_flask/search_server.py b/code_search/app/code_search/nmslib/search_server.py similarity index 80% rename from code_search/indexing_server/nmslib_flask/search_server.py rename to code_search/app/code_search/nmslib/search_server.py index 8daac87e..8c00cbf4 100644 --- a/code_search/indexing_server/nmslib_flask/search_server.py +++ b/code_search/app/code_search/nmslib/search_server.py @@ -1,10 +1,11 @@ from flask import Flask, request, abort, jsonify, make_response +from code_search.nmslib.search_engine import CodeSearchEngine class CodeSearchServer: """This utility class wraps the search engine into an HTTP server based on Flask""" - def __init__(self, engine, host='0.0.0.0', port=8008): + def __init__(self, engine: CodeSearchEngine, host='0.0.0.0', port=8008): self.app = Flask(__name__) self.host = host self.port = port @@ -24,7 +25,7 @@ class CodeSearchServer: abort(make_response( jsonify(status=400, error="empty query"), 400)) - result = self.engine.search(query_str) + result = self.engine.query(query_str) return make_response(jsonify(result=result)) def run(self): diff --git a/code_search/app/code_search/t2t/__init__.py b/code_search/app/code_search/t2t/__init__.py new file mode 100644 index 00000000..a3afc399 --- /dev/null +++ b/code_search/app/code_search/t2t/__init__.py @@ -0,0 +1 @@ +import code_search.t2t.similarity_transformer diff --git a/code_search/app/code_search/t2t/query.py b/code_search/app/code_search/t2t/query.py new file mode 100644 index 00000000..e8bb72d9 --- /dev/null +++ b/code_search/app/code_search/t2t/query.py @@ -0,0 +1,32 @@ +import base64 +import tensorflow as tf +from tensor2tensor.data_generators import text_encoder +from tensor2tensor.utils import registry + + +def get_encoder_decoder(problem_name, data_dir): + """Get encoder from the T2T problem.This might + vary by problem, keeping generic as a reference + """ + problem = registry.problem(problem_name) + hparams = tf.contrib.training.HParams(data_dir=data_dir) + problem.get_hparams(hparams) + return problem.feature_info["inputs"].encoder, \ + problem.feature_info["targets"].encoder + + +def encode_query(encoder, query_str): + """Encode the input query string using encoder. This + might vary by problem but keeping generic as a reference. + Note that in T2T problems, the 'targets' key is needed + even though it is ignored during inference. + See tensorflow/tensor2tensor#868""" + + encoded_str = encoder.encode(query_str) + [text_encoder.EOS_ID] + features = {"inputs": tf.train.Feature(int64_list=tf.train.Int64List(value=encoded_str)), + "targets": tf.train.Feature(int64_list=tf.train.Int64List(value=[0]))} + example = tf.train.Example(features=tf.train.Features(feature=features)) + return base64.b64encode(example.SerializeToString()).decode('utf-8') + +def decode_result(decoder, list_ids): + return decoder.decode(list_ids) diff --git a/code_search/language_task/t2t_problems/similarity_transformer.py b/code_search/app/code_search/t2t/similarity_transformer.py similarity index 100% rename from code_search/language_task/t2t_problems/similarity_transformer.py rename to code_search/app/code_search/t2t/similarity_transformer.py diff --git a/code_search/app/requirements.txt b/code_search/app/requirements.txt new file mode 100644 index 00000000..180cfb92 --- /dev/null +++ b/code_search/app/requirements.txt @@ -0,0 +1,8 @@ +tensor2tensor~=1.6.0 +tensorflow~=1.8.0 +oauth2client~=4.1.0 +Flask~=1.0.0 +nmslib~=1.7.0 +numpy~=1.14.0 +google-cloud-storage~=1.10.0 +requests~=2.18.0 diff --git a/code_search/indexing_server/setup.py b/code_search/app/setup.py similarity index 67% rename from code_search/indexing_server/setup.py rename to code_search/app/setup.py index 2455f51c..80774f51 100644 --- a/code_search/indexing_server/setup.py +++ b/code_search/app/setup.py @@ -3,10 +3,10 @@ from setuptools import setup, find_packages with open('requirements.txt', 'r') as f: install_requires = f.readlines() -VERSION = '0.1.0' +VERSION = '0.0.1' -setup(name='code-search-index-server', - description='Kubeflow Code Search Demo - Index Server', +setup(name='code-search', + description='Kubeflow Code Search Demo', url='https://www.github.com/kubeflow/examples', author='Sanyam Kapoor', author_email='sanyamkapoor@google.com', @@ -17,7 +17,7 @@ setup(name='code-search-index-server', extras_require={}, entry_points={ 'console_scripts': [ - 'nmslib-serve=nmslib_flask.cli:server', - 'nmslib-create=nmslib_flask.cli:creator', + 'nmslib-serve=code_search.nmslib.cli:server', + 'nmslib-create=code_search.nmslib.cli:creator', ] }) diff --git a/code_search/language_task/t2t-entrypoint.sh b/code_search/app/t2t-entrypoint.sh similarity index 100% rename from code_search/language_task/t2t-entrypoint.sh rename to code_search/app/t2t-entrypoint.sh diff --git a/code_search/language_task/build_image.sh b/code_search/build_image.sh similarity index 80% rename from code_search/language_task/build_image.sh rename to code_search/build_image.sh index 714b9caf..8f087b12 100755 --- a/code_search/language_task/build_image.sh +++ b/code_search/build_image.sh @@ -1,5 +1,11 @@ #!/usr/bin/env bash +## +# This script builds and pushes a Docker image containing +# "app" to Google Container Registry. It automatically tags +# a unique image for every run. +# + set -ex PROJECT=${PROJECT:-} @@ -17,7 +23,7 @@ BUILD_IMAGE_TAG="code-search:v$(date +%Y%m%d)$([[ ${GPU} = "1" ]] && echo '-gpu' # Directory of this script used as docker context _SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -pushd "$_SCRIPT_DIR" +pushd "${_SCRIPT_DIR}/app" docker build -t ${BUILD_IMAGE_TAG} --build-arg BASE_IMAGE_TAG=${BASE_IMAGE_TAG} . diff --git a/code_search/indexing_server/Dockerfile b/code_search/indexing_server/Dockerfile deleted file mode 100644 index 218d5707..00000000 --- a/code_search/indexing_server/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM python:3.6 - -ADD . /app - -WORKDIR /app - -RUN pip install . - -ENTRYPOINT ["sh"] diff --git a/code_search/indexing_server/build_image.sh b/code_search/indexing_server/build_image.sh deleted file mode 100755 index 3f7ec05b..00000000 --- a/code_search/indexing_server/build_image.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash - -set -e - -PROJECT=${PROJECT:-} -BUILD_IMAGE_TAG=${BUILD_IMAGE_TAG:-nmslib:devel} - -# Directory of this script used as docker context -_SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -pushd "$_SCRIPT_DIR" - -docker build -t ${BUILD_IMAGE_TAG} . - -# Push image to GCR if PROJECT available -if [[ ! -z "${PROJECT}" ]]; then - docker tag ${BUILD_IMAGE_TAG} gcr.io/${PROJECT}/${BUILD_IMAGE_TAG} - docker push gcr.io/${PROJECT}/${BUILD_IMAGE_TAG} -fi - -popd diff --git a/code_search/indexing_server/nmslib_flask/cli.py b/code_search/indexing_server/nmslib_flask/cli.py deleted file mode 100644 index 4003dc2f..00000000 --- a/code_search/indexing_server/nmslib_flask/cli.py +++ /dev/null @@ -1,71 +0,0 @@ -import sys -import os -import argparse -import numpy as np -from nmslib_flask.gcs import maybe_download_gcs_file, maybe_upload_gcs_file -from nmslib_flask.search_engine import CodeSearchEngine -from nmslib_flask.search_server import CodeSearchServer - -def parse_server_args(args): - parser = argparse.ArgumentParser(prog='nmslib Flask Server') - - parser.add_argument('--index-file', type=str, required=True, - help='Path to index file created by nmslib') - parser.add_argument('--data-file', type=str, required=True, - help='Path to csv file for human-readable data') - parser.add_argument('--data-dir', type=str, metavar='', default='/tmp', - help='Path to working data directory') - parser.add_argument('--host', type=str, metavar='', default='0.0.0.0', - help='Host to start server on') - parser.add_argument('--port', type=int, metavar='', default=8008, - help='Port to bind server to') - - return parser.parse_args(args) - - -def parse_creator_args(args): - parser = argparse.ArgumentParser(prog='nmslib Index Creator') - - parser.add_argument('--data-file', type=str, required=True, - help='Path to csv data file for human-readable data') - parser.add_argument('--output-file', type=str, metavar='', default='/tmp/index.nmslib', - help='Path to output index file') - parser.add_argument('--data-dir', type=str, metavar='', default='/tmp', - help='Path to working data directory') - - return parser.parse_args(args) - -def server(): - args = parse_server_args(sys.argv[1:]) - - if not os.path.isdir(args.data_dir): - os.makedirs(args.data_dir, exist_ok=True) - - # Download relevant files if needed - index_file = maybe_download_gcs_file(args.index_file, args.data_dir) - data_file = maybe_download_gcs_file(args.data_file, args.data_dir) - - search_engine = CodeSearchEngine(index_file, data_file) - - search_server = CodeSearchServer(engine=search_engine, - host=args.host, port=args.port) - search_server.run() - - -def creator(): - args = parse_creator_args(sys.argv[1:]) - - if not os.path.isdir(args.data_dir): - os.makedirs(args.data_dir, exist_ok=True) - - data_file = maybe_download_gcs_file(args.data_file, args.data_dir) - - # TODO(sanyamkapoor): parse data file into a numpy array - - data = np.load(data_file) - - tmp_output_file = os.path.join(args.data_dir, os.path.basename(args.output_file)) - - CodeSearchEngine.create_index(data, tmp_output_file) - - maybe_upload_gcs_file(tmp_output_file, args.output_file) diff --git a/code_search/indexing_server/nmslib_flask/search_engine.py b/code_search/indexing_server/nmslib_flask/search_engine.py deleted file mode 100644 index 4dfca1a5..00000000 --- a/code_search/indexing_server/nmslib_flask/search_engine.py +++ /dev/null @@ -1,46 +0,0 @@ -import nmslib -import numpy as np - - -class CodeSearchEngine: - """This is a utility class which takes an nmslib - index file and a data file to return data from""" - def __init__(self, index_file: str, data_file: str): - self._index_file = index_file - self._data_file = data_file - - self.index = CodeSearchEngine.nmslib_init() - self.index.loadIndex(index_file) - - # TODO: load the reverse-index map for actual code data - # self.data_map = - - def embed(self, query_str): - # TODO load trained model and embed input strings - raise NotImplementedError - - def query(self, query_str: str, k=2): - embedding = self.embed(query_str) - idxs, dists = self.index.knnQuery(embedding, k=k) - - # TODO(sanyamkapoor): initialize data map and return - # list of dicts - # [ - # {'src': self.data_map[idx], 'dist': dist} - # for idx, dist in zip(idxs, dists) - # ] - return idxs, dists - - @staticmethod - def nmslib_init(): - """Initializes an nmslib index object""" - index = nmslib.init(method='hnsw', space='cosinesimil') - return index - - @staticmethod - def create_index(data: np.array, save_path: str): - """Add numpy data to the index and save to path""" - index = CodeSearchEngine.nmslib_init() - index.addDataPointBatch(data) - index.createIndex({'post': 2}, print_progress=True) - index.saveIndex(save_path) diff --git a/code_search/indexing_server/requirements.txt b/code_search/indexing_server/requirements.txt deleted file mode 100644 index d69a7c43..00000000 --- a/code_search/indexing_server/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -Flask~=1.0.0 -nmslib~=1.7.0 -numpy~=1.14.0 -google-cloud-storage~=1.10.0 diff --git a/code_search/kubeflow/app.yaml b/code_search/kubeflow/app.yaml index 8f7b6a64..ace7ad86 100644 --- a/code_search/kubeflow/app.yaml +++ b/code_search/kubeflow/app.yaml @@ -14,6 +14,12 @@ libraries: refSpec: master name: tf-job registry: kubeflow + tf-serving: + gitVersion: + commitSha: e1b2aee865866b2e7e4f8c41b34ae03b4c4bb0db + refSpec: master + name: tf-serving + registry: kubeflow name: kubeflow registries: incubator: diff --git a/code_search/kubeflow/components/nms-creator.jsonnet b/code_search/kubeflow/components/nms-creator.jsonnet new file mode 100644 index 00000000..ed52fa29 --- /dev/null +++ b/code_search/kubeflow/components/nms-creator.jsonnet @@ -0,0 +1,7 @@ +local k = import "k.libsonnet"; +local nms = import "nms.libsonnet"; + +local env = std.extVar("__ksonnet/environments"); +local params = std.extVar("__ksonnet/params").components["nms-creator"]; + +std.prune(k.core.v1.list.new(nms.parts(params, env).creator)) diff --git a/code_search/kubeflow/components/nms-server.jsonnet b/code_search/kubeflow/components/nms-server.jsonnet new file mode 100644 index 00000000..781c6c36 --- /dev/null +++ b/code_search/kubeflow/components/nms-server.jsonnet @@ -0,0 +1,7 @@ +local k = import "k.libsonnet"; +local nms = import "nms.libsonnet"; + +local env = std.extVar("__ksonnet/environments"); +local params = std.extVar("__ksonnet/params").components["nms-server"]; + +std.prune(k.core.v1.list.new(nms.parts(params, env).server)) diff --git a/code_search/kubeflow/components/nms.libsonnet b/code_search/kubeflow/components/nms.libsonnet new file mode 100644 index 00000000..09837081 --- /dev/null +++ b/code_search/kubeflow/components/nms.libsonnet @@ -0,0 +1,119 @@ +local baseParams = std.extVar("__ksonnet/params").components["nmslib"]; + +{ + + nmsContainer(params, env):: { + apiVersion: "extensions/v1beta1", + kind: "Deployment", + metadata: { + name: params.name + "-deployment", + namespace: env.namespace, + labels: { + app: params.name, + } + }, + spec: { + replicas: params.replicas, + selector: { + matchLabels: { + app: params.name, + }, + }, + template: { + metadata: { + labels: { + app: params.name, + } + }, + spec: { + containers: [ + { + name: params.name, + image: params.image, + args: params.args, + ports: [ + { + containerPort: 8008, + } + ], + } + ], + }, + }, + }, + }, + + service(params, env):: { + apiVersion: "v1", + kind: "Service", + metadata: { + labels: { + app: params.name, + }, + name: params.name, + namespace: env.namespace, + annotations: { + "getambassador.io/config": + std.join("\n", [ + "---", + "apiVersion: ambassador/v0", + "kind: Mapping", + "name: http-mapping-" + params.name, + "prefix: /code-search/", + "rewrite: /", + "method: GET", + "service: " + params.name + "." + env.namespace + ":8008", + ]), + }, + }, + spec: { + type: "ClusterIP", + selector: { + app: params.name, + }, + ports: [ + { + name: "nmslib-serve-http", + port: 8008, + targetPort: 8008, + }, + ], + }, + }, + + parts(newParams, env):: { + local params = baseParams + newParams, + + creator:: { + local creatorParams = params + { + args: [ + "nmslib-create", + "--data-file=" + params.dataFile, + "--index-file=" + params.indexFile, + ], + }, + + all: [ + $.nmsContainer(creatorParams, env), + ], + }.all, + + server:: { + local serverParams = params + { + args: [ + "nmslib-serve", + "--data-file=" + params.dataFile, + "--index-file=" + params.indexFile, + "--problem=" + params.problem, + "--data-dir=" + params.dataDir, + "--serving-url=" + params.servingUrl, + ], + }, + + all: [ + $.service(serverParams, env), + $.nmsContainer(serverParams, env), + ], + }.all, + } +} diff --git a/code_search/kubeflow/components/params.libsonnet b/code_search/kubeflow/components/params.libsonnet index 53a03ba4..93141650 100644 --- a/code_search/kubeflow/components/params.libsonnet +++ b/code_search/kubeflow/components/params.libsonnet @@ -32,6 +32,18 @@ gsOutputDir: "null", }, + "nmslib": { + name: null, + replicas: 1, + image: "gcr.io/kubeflow-dev/code-search:v20180621-266e689", + + dataFile: null, + indexFile: null, + problem: null, + dataDir: null, + servingUrl: null, + }, + "t2t-translate-datagen": { jobType: "datagen", @@ -70,5 +82,22 @@ model: "transformer", hparams_set: "transformer_base_single_gpu", }, + + "t2t-translate-serving": { + name: "t2t-translate", + modelName: "t2t-translate", + modelPath: "gs://kubeflow-examples/t2t-translate/translate_ende_wmt32k/output/export/Servo", + modelServerImage: "gcr.io/kubeflow-images-public/tensorflow-serving-1.8:latest", + cloud: "gcp", + gcpCredentialSecretName: "gcp-credentials", + }, + + "nms-creator": { + name: "nms-creator", + }, + + "nms-server": { + name: "nms-server", + }, }, } diff --git a/code_search/kubeflow/components/t2t-translate-serving.jsonnet b/code_search/kubeflow/components/t2t-translate-serving.jsonnet new file mode 100644 index 00000000..96f5d4ff --- /dev/null +++ b/code_search/kubeflow/components/t2t-translate-serving.jsonnet @@ -0,0 +1,21 @@ +local env = std.extVar("__ksonnet/environments"); +local params = std.extVar("__ksonnet/params").components["t2t-translate-serving"]; + +local k = import "k.libsonnet"; + +// ksonnet appears to require name be a parameter of the prototype which is why we handle it differently. +local name = params.name; + +// updatedParams includes the namespace from env by default. +// We can override namespace in params if needed +local updatedParams = env + params; + +local tfServingBase = import "kubeflow/tf-serving/tf-serving.libsonnet"; +local tfServing = tfServingBase { + // Override parameters with user supplied parameters. + params+: updatedParams { + name: name, + }, +}; + +std.prune(k.core.v1.list.new(tfServing.components)) diff --git a/code_search/kubeflow/vendor/kubeflow/tf-serving/README.md b/code_search/kubeflow/vendor/kubeflow/tf-serving/README.md new file mode 100644 index 00000000..3cf13ada --- /dev/null +++ b/code_search/kubeflow/vendor/kubeflow/tf-serving/README.md @@ -0,0 +1,73 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [tf-serving](#tf-serving) + - [Quickstart](#quickstart) + - [Using the library](#using-the-library) + - [io.ksonnet.pkg.tf-serving](#ioksonnetpkgtf-serving) + - [Example](#example) + - [Parameters](#parameters) + + + +# tf-serving + +> TensorFlow serving is a server for TensorFlow models. + + +* [Quickstart](#quickstart) +* [Using Prototypes](#using-prototypes) + * [io.ksonnet.pkg.tf-serving](#io.ksonnet.pkg.tf-serving) + +## Quickstart + +*The following commands use the `io.ksonnet.pkg.tf-serving` prototype to generate Kubernetes YAML for tf-serving, and then deploys it to your Kubernetes cluster.* + +First, create a cluster and install the ksonnet CLI (see root-level [README.md](rootReadme)). + +If you haven't yet created a [ksonnet application](linkToSomewhere), do so using `ks init `. + +Finally, in the ksonnet application directory, run the following: + +```shell +# Expand prototype as a Jsonnet file, place in a file in the +# `components/` directory. (YAML and JSON are also available.) +$ ks prototype use io.ksonnet.pkg.tf-serving tf-serving \ + --name tf-serving \ + --namespace default + +# Apply to server. +$ ks apply -f tf-serving.jsonnet +``` + +## Using the library + +The library files for tf-serving define a set of relevant *parts* (_e.g._, deployments, services, secrets, and so on) that can be combined to configure tf-serving for a wide variety of scenarios. For example, a database like Redis may need a secret to hold the user password, or it may have no password if it's acting as a cache. + +This library provides a set of pre-fabricated "flavors" (or "distributions") of tf-serving, each of which is configured for a different use case. These are captured as ksonnet *prototypes*, which allow users to interactively customize these distributions for their specific needs. + +These prototypes, as well as how to use them, are enumerated below. + +### io.ksonnet.pkg.tf-serving + +TensorFlow serving +#### Example + +```shell +# Expand prototype as a Jsonnet file, place in a file in the +# `components/` directory. (YAML and JSON are also available.) +$ ks prototype use io.ksonnet.pkg.tf-serving tf-serving \ + --name YOUR_NAME_HERE \ + --model_path YOUR_MODEL_PATH_HERE +``` + +#### Parameters + +The available options to pass prototype are: + +* `--name=`: Name to give to each of the components [string] +* `--model_path=`: Path to the model. This can be a GCS path. [string] + + +[rootReadme]: https://github.com/ksonnet/mixins diff --git a/code_search/kubeflow/vendor/kubeflow/tf-serving/parts.yaml b/code_search/kubeflow/vendor/kubeflow/tf-serving/parts.yaml new file mode 100644 index 00000000..89955568 --- /dev/null +++ b/code_search/kubeflow/vendor/kubeflow/tf-serving/parts.yaml @@ -0,0 +1,35 @@ +{ + "name": "tf-serving", + "apiVersion": "0.0.1", + "kind": "ksonnet.io/parts", + "description": "TensorFlow serving is a server for TensorFlow models.\n", + "author": "kubeflow team ", + "contributors": [ + { + "name": "Jeremy Lewi", + "email": "jlewi@google.com" + } + ], + "repository": { + "type": "git", + "url": "https://github.com/kubeflow/kubeflow" + }, + "bugs": { + "url": "https://github.com/kubeflow/kubeflow/issues" + }, + "keywords": [ + "kubeflow", + "tensorflow", + "database" + ], + "quickStart": { + "prototype": "io.ksonnet.pkg.tf-serving", + "componentName": "tf-serving", + "flags": { + "name": "tf-serving", + "namespace": "default" + }, + "comment": "Run TensorFlow Serving" + }, + "license": "Apache 2.0" +} diff --git a/code_search/kubeflow/vendor/kubeflow/tf-serving/prototypes/tf-serving-all-features.jsonnet b/code_search/kubeflow/vendor/kubeflow/tf-serving/prototypes/tf-serving-all-features.jsonnet new file mode 100644 index 00000000..24aeeef7 --- /dev/null +++ b/code_search/kubeflow/vendor/kubeflow/tf-serving/prototypes/tf-serving-all-features.jsonnet @@ -0,0 +1,24 @@ +// @apiVersion 0.1 +// @name io.ksonnet.pkg.tf-serving +// @description TensorFlow serving +// @shortDescription A TensorFlow serving deployment +// @param name string Name to give to each of the components + +local k = import "k.libsonnet"; + +// ksonnet appears to require name be a parameter of the prototype which is why we handle it differently. +local name = import "param://name"; + +// updatedParams includes the namespace from env by default. +// We can override namespace in params if needed +local updatedParams = env + params; + +local tfServingBase = import "kubeflow/tf-serving/tf-serving.libsonnet"; +local tfServing = tfServingBase { + // Override parameters with user supplied parameters. + params+: updatedParams { + name: name, + }, +}; + +std.prune(k.core.v1.list.new(tfServing.components)) diff --git a/code_search/kubeflow/vendor/kubeflow/tf-serving/tf-serving.libsonnet b/code_search/kubeflow/vendor/kubeflow/tf-serving/tf-serving.libsonnet new file mode 100644 index 00000000..8c867452 --- /dev/null +++ b/code_search/kubeflow/vendor/kubeflow/tf-serving/tf-serving.libsonnet @@ -0,0 +1,387 @@ +{ + util:: import "kubeflow/tf-serving/util.libsonnet", + + // Parameters are intended to be late bound. + params:: { + name: null, + numGpus: 0, + labels: { + app: $.params.name, + }, + modelName: $.params.name, + modelPath: null, + modelStorageType: "cloud", + + version: "v1", + firstVersion: true, + + deployIstio: false, + + deployHttpProxy: false, + defaultHttpProxyImage: "gcr.io/kubeflow-images-public/tf-model-server-http-proxy:v20180606-9dfda4f2", + httpProxyImage: "", + httpProxyImageToUse: if $.params.httpProxyImage == "" then + $.params.defaultHttpProxyImage + else + $.params.httpProxyImage, + + serviceType: "ClusterIP", + + // If users want to override the image then can override defaultCpuImage and/or defaultGpuImage + // in which case the image used will still depend on whether GPUs are used or not. + // Users can also override modelServerImage in which case the user supplied value will always be used + // regardless of numGpus. + defaultCpuImage: "gcr.io/kubeflow-images-public/tensorflow-serving-1.7:v20180604-0da89b8a", + defaultGpuImage: "gcr.io/kubeflow-images-public/tensorflow-serving-1.6gpu:v20180604-0da89b8a", + modelServerImage: if $.params.numGpus == 0 then + $.params.defaultCpuImage + else + $.params.defaultGpuImage, + + + // Whether or not to enable s3 parameters + s3Enable:: false, + + // Which cloud to use + cloud:: null, + }, + + // Parametes specific to GCP. + gcpParams:: { + gcpCredentialSecretName: "", + } + $.params, + + // Parameters that control S3 access + // params overrides s3params because params can be overwritten by the user to override the defaults. + s3params:: { + // Name of the k8s secrets containing S3 credentials + s3SecretName: "", + // Name of the key in the k8s secret containing AWS_ACCESS_KEY_ID. + s3SecretAccesskeyidKeyName: "", + + // Name of the key in the k8s secret containing AWS_SECRET_ACCESS_KEY. + s3SecretSecretaccesskeyKeyName: "", + + // S3 region + s3AwsRegion: "us-west-1", + + // TODO(jlewi): We should use util.toBool to automatically conver to actual boolean values. + // The use of strings is left over from when they were prototype parameters which only supports string type. + + // true Whether or not to use https for S3 connections + s3UseHttps: "true", + + // Whether or not to verify https certificates for S3 connections + s3VerifySsl: "true", + + // URL for your s3-compatible endpoint. + s3Endpoint: "http://s3.us-west-1.amazonaws.com,", + } + $.params, + + + components:: { + + all:: [ + // Default routing rule for the first version of model. + if $.util.toBool($.params.deployIstio) && $.util.toBool($.params.firstVersion) then + $.parts.defaultRouteRule, + ] + + // TODO(jlewi): It would be better to structure s3 as a mixin. + // As an example it would be great to allow S3 and GCS parameters + // to be enabled simultaneously. This should be doable because + // each entails adding a set of environment variables and volumes + // to the containers. These volumes/environment variables shouldn't + // overlap so there's no reason we shouldn't be able to just add + // both modifications to the base container. + // I think we want to restructure things as mixins so they can just + // be added. + if $.params.s3Enable then + [ + $.s3parts.tfService, + $.s3parts.tfDeployment, + ] + else if $.params.cloud == "gcp" then + [ + $.gcpParts.tfService, + $.gcpParts.tfDeployment, + ] + else + [ + $.parts.tfService, + $.parts.tfDeployment, + ], + }.all, + + parts:: { + // We define the containers one level beneath parts because combined with jsonnet late binding + // this makes it easy for users to override specific bits of the container. + tfServingContainerBase:: { + name: $.params.name, + image: $.params.modelServerImage, + imagePullPolicy: "IfNotPresent", + args: [ + "/usr/bin/tensorflow_model_server", + "--port=9000", + "--rest_api_port=8000", + "--model_name=" + $.params.modelName, + "--model_base_path=" + $.params.modelPath, + ], + ports: [ + { + containerPort: 9000, + }, + { + containerPort: 8000, + }, + ], + // TODO(jlewi): We should add readiness and liveness probes. I think the blocker is that + // model-server doesn't have something we can use out of the box. + resources: { + requests: { + memory: "1Gi", + cpu: "1", + }, + limits: { + memory: "4Gi", + cpu: "4", + }, + }, + // The is user and group should be defined in the Docker image. + // Per best practices we don't run as the root user. + securityContext: { + runAsUser: 1000, + fsGroup: 1000, + }, + volumeMounts+: if $.params.modelStorageType == "nfs" then [{ + name: "nfs", + mountPath: "/mnt", + }] + else [], + }, // tfServingContainer + + tfServingContainer+: $.parts.tfServingContainerBase + + if $.params.numGpus > 0 then + { + resources+: { + limits+: { + "nvidia.com/gpu": $.params.numGpus, + }, + }, + } + else {}, + + tfServingMetadata+: { + labels: $.params.labels { version: $.params.version }, + annotations: { + "sidecar.istio.io/inject": if $.util.toBool($.params.deployIstio) then "true", + }, + }, + + httpProxyContainer:: { + name: $.params.name + "-http-proxy", + image: $.params.httpProxyImageToUse, + imagePullPolicy: "IfNotPresent", + command: [ + "python", + "/usr/src/app/server.py", + "--port=8000", + "--rpc_port=9000", + "--rpc_timeout=10.0", + ], + env: [], + ports: [ + { + containerPort: 8000, + }, + ], + resources: { + requests: { + memory: "1Gi", + cpu: "1", + }, + limits: { + memory: "4Gi", + cpu: "4", + }, + }, + securityContext: { + runAsUser: 1000, + fsGroup: 1000, + }, + }, // httpProxyContainer + + + tfDeployment: { + apiVersion: "extensions/v1beta1", + kind: "Deployment", + metadata: { + name: $.params.name + "-" + $.params.version, + namespace: $.params.namespace, + labels: $.params.labels, + }, + spec: { + template: { + metadata: $.parts.tfServingMetadata, + spec: { + containers: [ + $.parts.tfServingContainer, + if $.util.toBool($.params.deployHttpProxy) then + $.parts.httpProxyContainer, + ], + volumes+: if $.params.modelStorageType == "nfs" then + [{ + name: "nfs", + persistentVolumeClaim: { + claimName: $.params.nfsPVC, + }, + }] + else [], + }, + }, + }, + }, // tfDeployment + + tfService: { + apiVersion: "v1", + kind: "Service", + metadata: { + labels: $.params.labels, + name: $.params.name, + namespace: $.params.namespace, + annotations: { + "getambassador.io/config": + std.join("\n", [ + "---", + "apiVersion: ambassador/v0", + "kind: Mapping", + "name: tfserving-mapping-" + $.params.name + "-get", + "prefix: /models/" + $.params.name + "/", + "rewrite: /", + "method: GET", + "service: " + $.params.name + "." + $.params.namespace + ":8000", + "---", + "apiVersion: ambassador/v0", + "kind: Mapping", + "name: tfserving-mapping-" + $.params.name + "-post", + "prefix: /models/" + $.params.name + "/", + "rewrite: /model/" + $.params.name + ":predict", + "method: POST", + "service: " + $.params.name + "." + $.params.namespace + ":8000", + ]), + }, //annotations + }, + spec: { + ports: [ + { + name: "grpc-tf-serving", + port: 9000, + targetPort: 9000, + }, + { + name: "http-tf-serving-proxy", + port: 8000, + targetPort: 8000, + }, + ], + selector: $.params.labels, + type: $.params.serviceType, + }, + }, // tfService + + defaultRouteRule: { + apiVersion: "config.istio.io/v1alpha2", + kind: "RouteRule", + metadata: { + name: $.params.name + "-default", + namespace: $.params.namespace, + }, + spec: { + destination: { + name: $.params.name, + }, + precedence: 0, + route: [ + { + labels: { version: $.params.version }, + }, + ], + }, + }, + + }, // parts + + // Parts specific to S3 + s3parts:: $.parts { + s3Env:: [ + { name: "AWS_ACCESS_KEY_ID", valueFrom: { secretKeyRef: { name: $.s3params.s3SecretName, key: $.s3params.s3SecretAccesskeyidKeyName } } }, + { name: "AWS_SECRET_ACCESS_KEY", valueFrom: { secretKeyRef: { name: $.s3params.s3SecretName, key: $.s3params.s3SecretSecretaccesskeyKeyName } } }, + { name: "AWS_REGION", value: $.s3params.s3AwsRegion }, + { name: "S3_REGION", value: $.s3params.s3AwsRegion }, + { name: "S3_USE_HTTPS", value: $.s3params.s3UseHttps }, + { name: "S3_VERIFY_SSL", value: $.s3params.s3VerifySsl }, + { name: "S3_ENDPOINT", value: $.s3params.s3Endpoint }, + ], + + tfServingContainer: $.parts.tfServingContainer { + env+: $.s3parts.s3Env, + }, + + tfDeployment: $.parts.tfDeployment { + spec: +{ + template: +{ + metadata: $.parts.tfServingMetadata, + spec: +{ + containers: [ + $.s3parts.tfServingContainer, + if $.util.toBool($.params.deployHttpProxy) then + $.parts.httpProxyContainer, + ], + }, + }, + }, + }, // tfDeployment + }, // s3parts + + // Parts specific to GCP + gcpParts:: $.parts { + gcpEnv:: [ + if $.gcpParams.gcpCredentialSecretName != "" then + { name: "GOOGLE_APPLICATION_CREDENTIALS", value: "/secret/gcp-credentials/key.json" }, + ], + + tfServingContainer: $.parts.tfServingContainer { + env+: $.gcpParts.gcpEnv, + volumeMounts+: [ + if $.gcpParams.gcpCredentialSecretName != "" then + { + name: "gcp-credentials", + mountPath: "/secret/gcp-credentials", + }, + ], + }, + + tfDeployment: $.parts.tfDeployment { + spec+: { + template+: { + metadata: $.parts.tfServingMetadata, + spec+: { + containers: [ + $.gcpParts.tfServingContainer, + if $.util.toBool($.params.deployHttpProxy) then + $.parts.httpProxyContainer, + ], + volumes: [ + if $.gcpParams.gcpCredentialSecretName != "" then + { + name: "gcp-credentials", + secret: { + secretName: $.gcpParams.gcpCredentialSecretName, + }, + }, + ], + }, + }, + }, + }, // tfDeployment + }, // gcpParts +} diff --git a/code_search/kubeflow/vendor/kubeflow/tf-serving/util.libsonnet b/code_search/kubeflow/vendor/kubeflow/tf-serving/util.libsonnet new file mode 100644 index 00000000..087e97cd --- /dev/null +++ b/code_search/kubeflow/vendor/kubeflow/tf-serving/util.libsonnet @@ -0,0 +1,25 @@ +// Some useful routines. +{ + // Convert a string to upper case. + upper:: function(x) { + local cp(c) = std.codepoint(c), + local upLetter(c) = if cp(c) >= 97 && cp(c) < 123 then + std.char(cp(c) - 32) + else c, + result:: std.join("", std.map(upLetter, std.stringChars(x))), + }.result, + + // Convert non-boolean types like string,number to a boolean. + // This is primarily intended for dealing with parameters that should be booleans. + toBool:: function(x) { + result:: + if std.type(x) == "boolean" then + x + else if std.type(x) == "string" then + $.upper(x) == "TRUE" + else if std.type(x) == "number" then + x != 0 + else + false, + }.result, +} diff --git a/code_search/language_task/requirements.txt b/code_search/language_task/requirements.txt deleted file mode 100644 index e50d298a..00000000 --- a/code_search/language_task/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -tensor2tensor~=1.6.0 -oauth2client~=4.1.0 \ No newline at end of file diff --git a/code_search/language_task/t2t_problems/__init__.py b/code_search/language_task/t2t_problems/__init__.py deleted file mode 100644 index cb8ef90c..00000000 --- a/code_search/language_task/t2t_problems/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from . import function_summarizer -from . import docstring_lm