diff --git a/Dockerfile.hpo b/Dockerfile.hpo index e6bcdc9..0db4788 100644 --- a/Dockerfile.hpo +++ b/Dockerfile.hpo @@ -25,7 +25,7 @@ WORKDIR /opt/app # Install packages needed for python to function correctly # Create the non root user, same as the one used in the build phase. -RUN microdnf install -y python3 \ +RUN microdnf install -y python3 gcc-c++ python3-devel \ && microdnf update -y \ && microdnf -y install shadow-utils \ && adduser -u 1001 -G root -s /usr/sbin/nologin default \ @@ -38,7 +38,7 @@ RUN microdnf install -y python3 \ USER 1001 # Install optuna to the default user -RUN python3 -m pip install --user optuna requests scikit-optimize jsonschema +RUN python3 -m pip install --user optuna requests scikit-optimize jsonschema click grpcio protobuf LABEL name="Kruize HPO" \ vendor="Red Hat" \ @@ -54,5 +54,6 @@ COPY --chown=1001:0 index.html /opt/app/ EXPOSE 8085 +EXPOSE 50051 ENTRYPOINT python3 -u src/service.py diff --git a/requirements.txt b/requirements.txt index 8df66c0..673c167 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,7 @@ optuna requests scikit-optimize -jsonschema \ No newline at end of file +jsonschema +grpcio +click +protobuf \ No newline at end of file diff --git a/scripts/cluster-helpers.sh b/scripts/cluster-helpers.sh index b72764d..a060e01 100644 --- a/scripts/cluster-helpers.sh +++ b/scripts/cluster-helpers.sh @@ -69,7 +69,7 @@ function docker_start() { check_prereq running ${SERVICE_STATUS_DOCKER} - ${CONTAINER_RUNTIME} run -d --name hpo_docker_container -p 8085:8085 ${HPO_CONTAINER_IMAGE} >/dev/null 2>&1 + ${CONTAINER_RUNTIME} run -d --name hpo_docker_container -p 8085:8085 -p 50051:50051 ${HPO_CONTAINER_IMAGE} >/dev/null 2>&1 check_err "Unexpected error occured. Service Stopped!" echo diff --git a/src/README.md b/src/README.md index c5d8437..46f1f87 100644 --- a/src/README.md +++ b/src/README.md @@ -46,3 +46,52 @@ The criteria for setting the experiment status is: | `success` | The experiment runs successfully without any error. | | `failure` | The experiment fails due to reason such as OOMKilled. | | `prune` | The experiment terminates due to reasons such as insufficient cpu and memory. | + + +## gRPC Client + +[`grpc_client.py`](./grpc_client.py) is a command line client that allows users to interact with the gRPC service. + +### Usage + +```shell +$ python3 ./grpc_client.py +Usage: grpc_client.py [OPTIONS] COMMAND [ARGS]... + + A HPO command line tool to allow interaction with HPO service + +Options: + --help Show this message and exit. + +Commands: + config Obtain a configuration set for a particular experiment trial + count Return a count of experiments currently running + list List names of all experiments currently running + new Create a new experiment + next Generate next configuration set for running experiment + result Update results for a particular experiment trial + show Show details of running experiment + +``` + +Commands provide interactive input for params or allow users to set params on the command line for scripted use; + +e.g. + +```shell +$ python3 ./grpc_client.py new + Experiment configuration file path: +``` + +or + +```shell +$ python3 ./grpc_client.py new --file=/tmp/hpo/newExperiment.json +``` + +> **_NOTE:_** The default host and port for the client is `localhost` and `50051`. If you wish to connect to a remote machine, or via a diferent port, please set the following environment variables, `HPO_HOST` and `HPO_PORT`. +> e.g. +> ```shell +> $ export HPO_HOST=remoteMachine.com +> $ export HPO_PORT=9191 +> ``` \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/exceptions.py b/src/exceptions.py new file mode 100644 index 0000000..6395bed --- /dev/null +++ b/src/exceptions.py @@ -0,0 +1,23 @@ +""" +Copyright (c) 2020, 2022 Red Hat, IBM Corporation and others. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +class Error(Exception): + """Base class for other exceptions""" + pass + +class ExperimentNotFoundError(Error): + """Raised when the input value is too small""" + pass \ No newline at end of file diff --git a/src/gRPC/__init__.py b/src/gRPC/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/gRPC/hpo_pb2.py b/src/gRPC/hpo_pb2.py new file mode 100644 index 0000000..bc4efd4 --- /dev/null +++ b/src/gRPC/hpo_pb2.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: hpo.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\thpo.proto\x12\nhelloworld\"\'\n\x16NumberExperimentsReply\x12\r\n\x05\x63ount\x18\x01 \x01(\x05\"+\n\x13NewExperimentsReply\x12\x14\n\x0ctrial_number\x18\x01 \x01(\x05\"*\n\x14\x45xperimentsListReply\x12\x12\n\nexperiment\x18\x01 \x03(\t\"\x19\n\x17NumberExperimentsParams\"\x17\n\x15\x45xperimentsListParams\"\x16\n\x14\x45xperimentTrialReply\"/\n\x14\x45xperimentNameParams\x12\x17\n\x0f\x65xperiment_name\x18\x01 \x01(\t\"9\n\x0f\x45xperimentTrial\x12\x17\n\x0f\x65xperiment_name\x18\x01 \x01(\t\x12\r\n\x05trial\x18\x02 \x01(\x05\"\xcb\x01\n\x15\x45xperimentTrialResult\x12\x17\n\x0f\x65xperiment_name\x18\x01 \x01(\t\x12\r\n\x05trial\x18\x02 \x01(\x05\x12\x38\n\x06result\x18\x03 \x01(\x0e\x32(.helloworld.ExperimentTrialResult.Result\x12\x12\n\nvalue_type\x18\x04 \x01(\t\x12\r\n\x05value\x18\x05 \x01(\x01\"-\n\x06Result\x12\x0b\n\x07SUCCESS\x10\x00\x12\x0b\n\x07\x46\x41ILURE\x10\x01\x12\t\n\x05PRUNE\x10\x02\"\x8f\x03\n\x11\x45xperimentDetails\x12\x17\n\x0f\x65xperiment_name\x18\x01 \x01(\t\x12\x14\n\x0ctotal_trials\x18\x02 \x01(\x05\x12\x17\n\x0fparallel_trials\x18\x03 \x01(\x05\x12\x11\n\tdirection\x18\x04 \x01(\t\x12\x15\n\rhpo_algo_impl\x18\x05 \x01(\t\x12\x15\n\rexperiment_id\x18\x06 \x01(\t\x12\x1a\n\x12objective_function\x18\x07 \x01(\t\x12\x38\n\ttuneables\x18\x08 \x03(\x0b\x32%.helloworld.ExperimentDetails.Tunable\x12\x12\n\nvalue_type\x18\t \x01(\t\x12\x11\n\tslo_class\x18\n \x01(\t\x12\x0f\n\x07started\x18\x0b \x01(\x08\x1a\x63\n\x07Tunable\x12\x12\n\nvalue_type\x18\x01 \x01(\t\x12\x13\n\x0blower_bound\x18\x02 \x01(\x05\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x13\n\x0bupper_bound\x18\x04 \x01(\x05\x12\x0c\n\x04step\x18\x05 \x01(\x01\",\n\rTunableConfig\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x02\"8\n\x0bTrialConfig\x12)\n\x06\x63onfig\x18\x01 \x03(\x0b\x32\x19.helloworld.TunableConfig2\xf5\x04\n\nHpoService\x12^\n\x11NumberExperiments\x12#.helloworld.NumberExperimentsParams\x1a\".helloworld.NumberExperimentsReply\"\x00\x12X\n\x0f\x45xperimentsList\x12!.helloworld.ExperimentsListParams\x1a .helloworld.ExperimentsListReply\"\x00\x12Q\n\rNewExperiment\x12\x1d.helloworld.ExperimentDetails\x1a\x1f.helloworld.NewExperimentsReply\"\x00\x12Y\n\x14GetExperimentDetails\x12 .helloworld.ExperimentNameParams\x1a\x1d.helloworld.ExperimentDetails\"\x00\x12H\n\x0eGetTrialConfig\x12\x1b.helloworld.ExperimentTrial\x1a\x17.helloworld.TrialConfig\"\x00\x12Z\n\x11UpdateTrialResult\x12!.helloworld.ExperimentTrialResult\x1a .helloworld.ExperimentTrialReply\"\x00\x12Y\n\x12GenerateNextConfig\x12 .helloworld.ExperimentNameParams\x1a\x1f.helloworld.NewExperimentsReply\"\x00\x42#\n\rio.kruize.hpoB\nHpoServiceP\x01\xa2\x02\x03HLWb\x06proto3') + + + +_NUMBEREXPERIMENTSREPLY = DESCRIPTOR.message_types_by_name['NumberExperimentsReply'] +_NEWEXPERIMENTSREPLY = DESCRIPTOR.message_types_by_name['NewExperimentsReply'] +_EXPERIMENTSLISTREPLY = DESCRIPTOR.message_types_by_name['ExperimentsListReply'] +_NUMBEREXPERIMENTSPARAMS = DESCRIPTOR.message_types_by_name['NumberExperimentsParams'] +_EXPERIMENTSLISTPARAMS = DESCRIPTOR.message_types_by_name['ExperimentsListParams'] +_EXPERIMENTTRIALREPLY = DESCRIPTOR.message_types_by_name['ExperimentTrialReply'] +_EXPERIMENTNAMEPARAMS = DESCRIPTOR.message_types_by_name['ExperimentNameParams'] +_EXPERIMENTTRIAL = DESCRIPTOR.message_types_by_name['ExperimentTrial'] +_EXPERIMENTTRIALRESULT = DESCRIPTOR.message_types_by_name['ExperimentTrialResult'] +_EXPERIMENTDETAILS = DESCRIPTOR.message_types_by_name['ExperimentDetails'] +_EXPERIMENTDETAILS_TUNABLE = _EXPERIMENTDETAILS.nested_types_by_name['Tunable'] +_TUNABLECONFIG = DESCRIPTOR.message_types_by_name['TunableConfig'] +_TRIALCONFIG = DESCRIPTOR.message_types_by_name['TrialConfig'] +_EXPERIMENTTRIALRESULT_RESULT = _EXPERIMENTTRIALRESULT.enum_types_by_name['Result'] +NumberExperimentsReply = _reflection.GeneratedProtocolMessageType('NumberExperimentsReply', (_message.Message,), { + 'DESCRIPTOR' : _NUMBEREXPERIMENTSREPLY, + '__module__' : 'hpo_pb2' + # @@protoc_insertion_point(class_scope:helloworld.NumberExperimentsReply) + }) +_sym_db.RegisterMessage(NumberExperimentsReply) + +NewExperimentsReply = _reflection.GeneratedProtocolMessageType('NewExperimentsReply', (_message.Message,), { + 'DESCRIPTOR' : _NEWEXPERIMENTSREPLY, + '__module__' : 'hpo_pb2' + # @@protoc_insertion_point(class_scope:helloworld.NewExperimentsReply) + }) +_sym_db.RegisterMessage(NewExperimentsReply) + +ExperimentsListReply = _reflection.GeneratedProtocolMessageType('ExperimentsListReply', (_message.Message,), { + 'DESCRIPTOR' : _EXPERIMENTSLISTREPLY, + '__module__' : 'hpo_pb2' + # @@protoc_insertion_point(class_scope:helloworld.ExperimentsListReply) + }) +_sym_db.RegisterMessage(ExperimentsListReply) + +NumberExperimentsParams = _reflection.GeneratedProtocolMessageType('NumberExperimentsParams', (_message.Message,), { + 'DESCRIPTOR' : _NUMBEREXPERIMENTSPARAMS, + '__module__' : 'hpo_pb2' + # @@protoc_insertion_point(class_scope:helloworld.NumberExperimentsParams) + }) +_sym_db.RegisterMessage(NumberExperimentsParams) + +ExperimentsListParams = _reflection.GeneratedProtocolMessageType('ExperimentsListParams', (_message.Message,), { + 'DESCRIPTOR' : _EXPERIMENTSLISTPARAMS, + '__module__' : 'hpo_pb2' + # @@protoc_insertion_point(class_scope:helloworld.ExperimentsListParams) + }) +_sym_db.RegisterMessage(ExperimentsListParams) + +ExperimentTrialReply = _reflection.GeneratedProtocolMessageType('ExperimentTrialReply', (_message.Message,), { + 'DESCRIPTOR' : _EXPERIMENTTRIALREPLY, + '__module__' : 'hpo_pb2' + # @@protoc_insertion_point(class_scope:helloworld.ExperimentTrialReply) + }) +_sym_db.RegisterMessage(ExperimentTrialReply) + +ExperimentNameParams = _reflection.GeneratedProtocolMessageType('ExperimentNameParams', (_message.Message,), { + 'DESCRIPTOR' : _EXPERIMENTNAMEPARAMS, + '__module__' : 'hpo_pb2' + # @@protoc_insertion_point(class_scope:helloworld.ExperimentNameParams) + }) +_sym_db.RegisterMessage(ExperimentNameParams) + +ExperimentTrial = _reflection.GeneratedProtocolMessageType('ExperimentTrial', (_message.Message,), { + 'DESCRIPTOR' : _EXPERIMENTTRIAL, + '__module__' : 'hpo_pb2' + # @@protoc_insertion_point(class_scope:helloworld.ExperimentTrial) + }) +_sym_db.RegisterMessage(ExperimentTrial) + +ExperimentTrialResult = _reflection.GeneratedProtocolMessageType('ExperimentTrialResult', (_message.Message,), { + 'DESCRIPTOR' : _EXPERIMENTTRIALRESULT, + '__module__' : 'hpo_pb2' + # @@protoc_insertion_point(class_scope:helloworld.ExperimentTrialResult) + }) +_sym_db.RegisterMessage(ExperimentTrialResult) + +ExperimentDetails = _reflection.GeneratedProtocolMessageType('ExperimentDetails', (_message.Message,), { + + 'Tunable' : _reflection.GeneratedProtocolMessageType('Tunable', (_message.Message,), { + 'DESCRIPTOR' : _EXPERIMENTDETAILS_TUNABLE, + '__module__' : 'hpo_pb2' + # @@protoc_insertion_point(class_scope:helloworld.ExperimentDetails.Tunable) + }) + , + 'DESCRIPTOR' : _EXPERIMENTDETAILS, + '__module__' : 'hpo_pb2' + # @@protoc_insertion_point(class_scope:helloworld.ExperimentDetails) + }) +_sym_db.RegisterMessage(ExperimentDetails) +_sym_db.RegisterMessage(ExperimentDetails.Tunable) + +TunableConfig = _reflection.GeneratedProtocolMessageType('TunableConfig', (_message.Message,), { + 'DESCRIPTOR' : _TUNABLECONFIG, + '__module__' : 'hpo_pb2' + # @@protoc_insertion_point(class_scope:helloworld.TunableConfig) + }) +_sym_db.RegisterMessage(TunableConfig) + +TrialConfig = _reflection.GeneratedProtocolMessageType('TrialConfig', (_message.Message,), { + 'DESCRIPTOR' : _TRIALCONFIG, + '__module__' : 'hpo_pb2' + # @@protoc_insertion_point(class_scope:helloworld.TrialConfig) + }) +_sym_db.RegisterMessage(TrialConfig) + +_HPOSERVICE = DESCRIPTOR.services_by_name['HpoService'] +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\rio.kruize.hpoB\nHpoServiceP\001\242\002\003HLW' + _NUMBEREXPERIMENTSREPLY._serialized_start=25 + _NUMBEREXPERIMENTSREPLY._serialized_end=64 + _NEWEXPERIMENTSREPLY._serialized_start=66 + _NEWEXPERIMENTSREPLY._serialized_end=109 + _EXPERIMENTSLISTREPLY._serialized_start=111 + _EXPERIMENTSLISTREPLY._serialized_end=153 + _NUMBEREXPERIMENTSPARAMS._serialized_start=155 + _NUMBEREXPERIMENTSPARAMS._serialized_end=180 + _EXPERIMENTSLISTPARAMS._serialized_start=182 + _EXPERIMENTSLISTPARAMS._serialized_end=205 + _EXPERIMENTTRIALREPLY._serialized_start=207 + _EXPERIMENTTRIALREPLY._serialized_end=229 + _EXPERIMENTNAMEPARAMS._serialized_start=231 + _EXPERIMENTNAMEPARAMS._serialized_end=278 + _EXPERIMENTTRIAL._serialized_start=280 + _EXPERIMENTTRIAL._serialized_end=337 + _EXPERIMENTTRIALRESULT._serialized_start=340 + _EXPERIMENTTRIALRESULT._serialized_end=543 + _EXPERIMENTTRIALRESULT_RESULT._serialized_start=498 + _EXPERIMENTTRIALRESULT_RESULT._serialized_end=543 + _EXPERIMENTDETAILS._serialized_start=546 + _EXPERIMENTDETAILS._serialized_end=945 + _EXPERIMENTDETAILS_TUNABLE._serialized_start=846 + _EXPERIMENTDETAILS_TUNABLE._serialized_end=945 + _TUNABLECONFIG._serialized_start=947 + _TUNABLECONFIG._serialized_end=991 + _TRIALCONFIG._serialized_start=993 + _TRIALCONFIG._serialized_end=1049 + _HPOSERVICE._serialized_start=1052 + _HPOSERVICE._serialized_end=1681 +# @@protoc_insertion_point(module_scope) diff --git a/src/gRPC/hpo_pb2_grpc.py b/src/gRPC/hpo_pb2_grpc.py new file mode 100644 index 0000000..e2ffdce --- /dev/null +++ b/src/gRPC/hpo_pb2_grpc.py @@ -0,0 +1,267 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +from . import hpo_pb2 as hpo__pb2 + + +class HpoServiceStub(object): + """The hpo service definition. + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.NumberExperiments = channel.unary_unary( + '/helloworld.HpoService/NumberExperiments', + request_serializer=hpo__pb2.NumberExperimentsParams.SerializeToString, + response_deserializer=hpo__pb2.NumberExperimentsReply.FromString, + ) + self.ExperimentsList = channel.unary_unary( + '/helloworld.HpoService/ExperimentsList', + request_serializer=hpo__pb2.ExperimentsListParams.SerializeToString, + response_deserializer=hpo__pb2.ExperimentsListReply.FromString, + ) + self.NewExperiment = channel.unary_unary( + '/helloworld.HpoService/NewExperiment', + request_serializer=hpo__pb2.ExperimentDetails.SerializeToString, + response_deserializer=hpo__pb2.NewExperimentsReply.FromString, + ) + self.GetExperimentDetails = channel.unary_unary( + '/helloworld.HpoService/GetExperimentDetails', + request_serializer=hpo__pb2.ExperimentNameParams.SerializeToString, + response_deserializer=hpo__pb2.ExperimentDetails.FromString, + ) + self.GetTrialConfig = channel.unary_unary( + '/helloworld.HpoService/GetTrialConfig', + request_serializer=hpo__pb2.ExperimentTrial.SerializeToString, + response_deserializer=hpo__pb2.TrialConfig.FromString, + ) + self.UpdateTrialResult = channel.unary_unary( + '/helloworld.HpoService/UpdateTrialResult', + request_serializer=hpo__pb2.ExperimentTrialResult.SerializeToString, + response_deserializer=hpo__pb2.ExperimentTrialReply.FromString, + ) + self.GenerateNextConfig = channel.unary_unary( + '/helloworld.HpoService/GenerateNextConfig', + request_serializer=hpo__pb2.ExperimentNameParams.SerializeToString, + response_deserializer=hpo__pb2.NewExperimentsReply.FromString, + ) + + +class HpoServiceServicer(object): + """The hpo service definition. + """ + + def NumberExperiments(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ExperimentsList(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def NewExperiment(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetExperimentDetails(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetTrialConfig(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def UpdateTrialResult(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GenerateNextConfig(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_HpoServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'NumberExperiments': grpc.unary_unary_rpc_method_handler( + servicer.NumberExperiments, + request_deserializer=hpo__pb2.NumberExperimentsParams.FromString, + response_serializer=hpo__pb2.NumberExperimentsReply.SerializeToString, + ), + 'ExperimentsList': grpc.unary_unary_rpc_method_handler( + servicer.ExperimentsList, + request_deserializer=hpo__pb2.ExperimentsListParams.FromString, + response_serializer=hpo__pb2.ExperimentsListReply.SerializeToString, + ), + 'NewExperiment': grpc.unary_unary_rpc_method_handler( + servicer.NewExperiment, + request_deserializer=hpo__pb2.ExperimentDetails.FromString, + response_serializer=hpo__pb2.NewExperimentsReply.SerializeToString, + ), + 'GetExperimentDetails': grpc.unary_unary_rpc_method_handler( + servicer.GetExperimentDetails, + request_deserializer=hpo__pb2.ExperimentNameParams.FromString, + response_serializer=hpo__pb2.ExperimentDetails.SerializeToString, + ), + 'GetTrialConfig': grpc.unary_unary_rpc_method_handler( + servicer.GetTrialConfig, + request_deserializer=hpo__pb2.ExperimentTrial.FromString, + response_serializer=hpo__pb2.TrialConfig.SerializeToString, + ), + 'UpdateTrialResult': grpc.unary_unary_rpc_method_handler( + servicer.UpdateTrialResult, + request_deserializer=hpo__pb2.ExperimentTrialResult.FromString, + response_serializer=hpo__pb2.ExperimentTrialReply.SerializeToString, + ), + 'GenerateNextConfig': grpc.unary_unary_rpc_method_handler( + servicer.GenerateNextConfig, + request_deserializer=hpo__pb2.ExperimentNameParams.FromString, + response_serializer=hpo__pb2.NewExperimentsReply.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'helloworld.HpoService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class HpoService(object): + """The hpo service definition. + """ + + @staticmethod + def NumberExperiments(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/helloworld.HpoService/NumberExperiments', + hpo__pb2.NumberExperimentsParams.SerializeToString, + hpo__pb2.NumberExperimentsReply.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ExperimentsList(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/helloworld.HpoService/ExperimentsList', + hpo__pb2.ExperimentsListParams.SerializeToString, + hpo__pb2.ExperimentsListReply.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def NewExperiment(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/helloworld.HpoService/NewExperiment', + hpo__pb2.ExperimentDetails.SerializeToString, + hpo__pb2.NewExperimentsReply.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetExperimentDetails(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/helloworld.HpoService/GetExperimentDetails', + hpo__pb2.ExperimentNameParams.SerializeToString, + hpo__pb2.ExperimentDetails.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetTrialConfig(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/helloworld.HpoService/GetTrialConfig', + hpo__pb2.ExperimentTrial.SerializeToString, + hpo__pb2.TrialConfig.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def UpdateTrialResult(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/helloworld.HpoService/UpdateTrialResult', + hpo__pb2.ExperimentTrialResult.SerializeToString, + hpo__pb2.ExperimentTrialReply.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GenerateNextConfig(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/helloworld.HpoService/GenerateNextConfig', + hpo__pb2.ExperimentNameParams.SerializeToString, + hpo__pb2.NewExperimentsReply.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/src/gRPC/protos/hpo.proto b/src/gRPC/protos/hpo.proto new file mode 100644 index 0000000..9f5ea58 --- /dev/null +++ b/src/gRPC/protos/hpo.proto @@ -0,0 +1,108 @@ +// Copyright (c) 2020, 2022 Red Hat, IBM Corporation and others. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + + +option java_multiple_files = true; +option java_package = "io.kruize.hpo"; +option java_outer_classname = "Hpo"; +option objc_class_prefix = "HLW"; + +package helloworld; + +// The hpo service definition. +service HpoService { + rpc NumberExperiments(NumberExperimentsParams) returns (NumberExperimentsReply) {} + rpc ExperimentsList(ExperimentsListParams) returns (ExperimentsListReply) {} + rpc NewExperiment(ExperimentDetails) returns (NewExperimentsReply) {} + rpc GetExperimentDetails(ExperimentNameParams) returns (ExperimentDetails) {} + rpc GetTrialConfig(ExperimentTrial) returns (TrialConfig) {} + rpc UpdateTrialResult(ExperimentTrialResult) returns (ExperimentTrialReply) {} + rpc GenerateNextConfig(ExperimentNameParams) returns (NewExperimentsReply) {} +} + +message NumberExperimentsReply { + int32 count = 1; +} + +message NewExperimentsReply { + int32 trial_number = 1; +} + +message ExperimentsListReply { + repeated string experiment = 1; +} + +message NumberExperimentsParams {} + +message ExperimentsListParams {} + +message ExperimentTrialReply{} + +message ExperimentNameParams { + string experiment_name = 1; +} + +message ExperimentTrial { + string experiment_name = 1; + int32 trial = 2; +} + +message ExperimentTrialResult{ + enum Result { + SUCCESS = 0; + FAILURE = 1; + PRUNE = 2; + } + + string experiment_name = 1; + int32 trial = 2; + Result result = 3; + string value_type = 4; + double value = 5; +} + +message ExperimentDetails { + message Tunable{ + string value_type = 1; + int32 lower_bound = 2; + string name = 3; + int32 upper_bound = 4; + double step = 5; + } + + string experiment_name = 1; + int32 total_trials = 2; + int32 parallel_trials = 3; + string direction = 4; + string hpo_algo_impl = 5; + string experiment_id = 6; + string objective_function= 7; + repeated Tunable tuneables = 8; + string value_type= 9; + string slo_class = 10; + bool started = 11; +} + +message TunableConfig { + string name = 1; + float value = 2; +} + +message TrialConfig { + repeated TunableConfig config = 1; +} + + diff --git a/src/grpc_client.py b/src/grpc_client.py new file mode 100644 index 0000000..5d0388b --- /dev/null +++ b/src/grpc_client.py @@ -0,0 +1,167 @@ +""" +Copyright (c) 2020, 2022 Red Hat, IBM Corporation and others. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from __future__ import print_function + +import logging +import os + +import click +import json + +import grpc +from gRPC import hpo_pb2_grpc, hpo_pb2 +from google.protobuf.json_format import MessageToJson, ParseError +from google.protobuf.json_format import Parse, ParseDict + +default_host_name="localhost" +default_server_port = 50051 +@click.group() +def main(): + """A HPO command line tool to allow interaction with HPO service""" + logging.basicConfig() + pass + +@main.command() +def count(): + """Return a count of experiments currently running""" + empty = hpo_pb2.NumberExperimentsReply() + fun = lambda stub: stub.NumberExperiments(empty) + response = run(fun) + click.echo(" Number of running experiments: {}".format(response.count)) + +@main.command() +def list(): + """List names of all experiments currently running""" + empty = hpo_pb2.NumberExperimentsReply() + fun = lambda stub: stub.ExperimentsList(empty) + experiments: hpo_pb2.ExperimentsListReply = run(fun) + print("Running Experiments:") + for experiment in experiments.experiment: + click.echo(" %s" % experiment) + + +@main.command() +@click.option("--name", prompt=" Experiment name", type=str) +def show(name): + """Show details of running experiment""" + expr: hpo_pb2.ExperimentNameParams = hpo_pb2.ExperimentNameParams() + expr.experiment_name = name + fun = lambda stub: stub.GetExperimentDetails(expr) + experiment: hpo_pb2.ExperimentDetails = run(fun) + json_obj = MessageToJson(experiment) + click.echo(json_obj) + +@main.command() +@click.option("--file", prompt=" Experiment configuration file path", type=str) +def new(file): + """Create a new experiment""" + # TODO: validate file path + with open(file, 'r') as json_file: + data = json.load(json_file) + try: + message: hpo_pb2.ExperimentDetails = ParseDict(data, hpo_pb2.ExperimentDetails()) + except ParseError as pErr : + raise click.ClickException("Unable to parse: " + file) + click.echo(" Adding new experiment: {}".format(message.experiment_name)) + fun = lambda stub: stub.NewExperiment(message) + response: hpo_pb2.NewExperimentsReply = run(fun) + click.echo("Trial Number: {}".format(response.trial_number)) + +@main.command() +@click.option("--name", prompt=" Experiment name", type=str) +@click.option("--trial", prompt=" Trial number", type=int) +def config(name, trial): + """Obtain a configuration set for a particular experiment trail""" + expr: hpo_pb2.ExperimentTrial = hpo_pb2.ExperimentTrial() + expr.experiment_name = name + expr.trial = trial + fun = lambda stub: stub.GetTrialConfig(expr) + trial_config: hpo_pb2.TrialConfig = run(fun) + json_obj = MessageToJson(trial_config) + click.echo(json_obj) + +@main.command() +@click.option("--name", prompt=" Enter name", type=str) +@click.option("--trial", prompt=" Enter trial number", type=int) +@click.option("--result", prompt=" Enter trial result", type=str) +@click.option("--value_type", prompt=" Enter result type", type=str) +@click.option("--value", prompt=" Enter result value", type=float) +def result(name, trial, result, value_type, value): + """Update results for a particular experiment trail""" + trialResult: hpo_pb2.ExperimentTrialResult = hpo_pb2.ExperimentTrialResult() + trialResult.experiment_name = name + trialResult.trial = trial + trialResult.result = hpo_pb2._EXPERIMENTTRIALRESULT_RESULT.values_by_name[result].number + trialResult.value_type = value_type + trialResult.value = value + fun = lambda stub: stub.UpdateTrialResult(trialResult) + hpo_pb2.TrialConfig = run(fun) + click.echo("Success: Updated Trial Result") + +@main.command() +@click.option("--name", prompt=" Enter name", type=str) +def next(name): + """Generate next configuration set for running experiment""" + experiment: hpo_pb2.ExperimentNameParams = hpo_pb2.ExperimentNameParams() + experiment.experiment_name = name + fun = lambda stub : stub.GenerateNextConfig(experiment) + reply: hpo_pb2.NewExperimentsReply = run(fun) + click.echo("Next Trial: {}".format(reply.trial_number)) + +def run(func): + # NOTE(gRPC Python Team): .close() is possible on a channel and should be + # used in circumstances in which the with statement does not fit the needs + # of the code. + + if "HPO_HOST" in os.environ: + host_name = os.environ.get('HPO_HOST') + else : + host_name = default_host_name + + if "HPO_PORT" in os.environ: + server_port = os.environ.get('HPO_PORT') + else : + server_port = default_server_port + + + + with grpc.insecure_channel(host_name + ':' + str(server_port)) as channel: + stub = hpo_pb2_grpc.HpoServiceStub(channel) + try: + response = func(stub) + except grpc.RpcError as rpc_error: + if rpc_error.code() == grpc.StatusCode.CANCELLED: + pass + elif rpc_error.code() == grpc.StatusCode.UNAVAILABLE: + pass + elif rpc_error.code() == grpc.StatusCode.NOT_FOUND: + raise click.ClickException(rpc_error.details()) + elif rpc_error.code() == grpc.StatusCode.INVALID_ARGUMENT: + raise click.ClickException(rpc_error.details()) + else: + raise click.ClickException("Received unknown RPC error: code={" + str(rpc_error.code()) + "} message={" + rpc_error.details() + "}") + return response + + +def NewExperiment(stub, **args): + empty = hpo_pb2.NumberExperimentsReply() + response = stub.NumberExperiments(empty) + click.echo("HpoService client received: %s" % response.count) + + +if __name__ == "__main__": + main() diff --git a/src/grpc_service.py b/src/grpc_service.py new file mode 100644 index 0000000..eab6cd5 --- /dev/null +++ b/src/grpc_service.py @@ -0,0 +1,136 @@ +""" +Copyright (c) 2020, 2022 Red Hat, IBM Corporation and others. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from concurrent import futures +import logging + +import grpc +import json +from gRPC import hpo_pb2, hpo_pb2_grpc +import hpo_service +from google.protobuf.json_format import MessageToJson +from bayes_optuna.optuna_hpo import HpoExperiment +from gRPC.hpo_pb2 import NewExperimentsReply +from exceptions import ExperimentNotFoundError + +host_name="0.0.0.0" +server_port = 50051 + +class HpoService(hpo_pb2_grpc.HpoServiceServicer): + + def NumberExperiments(self, request, context): + return hpo_pb2.NumberExperimentsReply(count=len(hpo_service.instance.experiments)) + + def ExperimentsList(self, request, context): + experiments = hpo_service.instance.getExperimentsList() + reply = hpo_pb2.ExperimentsListReply() + for experiment in experiments: + reply.experiment.extend([experiment]) + context.set_code(grpc.StatusCode.OK) + return reply + + def GetExperimentDetails(self, request, context): + try: + experiment: HpoExperiment = hpo_service.instance.getExperiment(request.experiment_name) + reply = hpo_pb2.ExperimentDetails() + reply.experiment_name = experiment.experiment_name + reply.direction = experiment.direction + reply.hpo_algo_impl = experiment.hpo_algo_impl + # reply.id_ = experiment.id_ + reply.objective_function = experiment.objective_function + # TODO:: expand tunables message + # reply.tunables = experiment.tunables + reply.started = experiment.started + context.set_code(grpc.StatusCode.OK) + return reply + except ExperimentNotFoundError: + context.set_code(grpc.StatusCode.NOT_FOUND) + context.set_details('Could not find experiment: %s' % request.experiment_name) + return + + + def NewExperiment(self, request, context): + if request.hpo_algo_impl in ("optuna_tpe", "optuna_tpe_multivariate", "optuna_skopt"): + tuneables = [] + for tuneable in request.tuneables: + tuneables.append(json.loads(MessageToJson(tuneable, preserving_proto_field_name=True))) + hpo_service.instance.newExperiment(None, request.experiment_name, + request.total_trials, request.parallel_trials, + request.direction, request.hpo_algo_impl, + request.objective_function, + tuneables, request.value_type) + hpo_service.instance.startExperiment(request.experiment_name) + experiment: HpoExperiment = hpo_service.instance.getExperiment(request.experiment_name) + reply: NewExperimentsReply = NewExperimentsReply() + reply.trial_number = experiment.trialDetails.trial_number + context.set_code(grpc.StatusCode.OK) + return reply + else: + context.set_code(grpc.StatusCode.INVALID_ARGUMENT) + context.set_details('Invalid algorithm: %s' % request.hpo_algo_impl) + return + + def GetTrialConfig(self, request, context): + if hpo_service.instance.containsExperiment(request.experiment_name): + data = json.loads(hpo_service.instance.get_trial_json_object(request.experiment_name)) + trialConfig : hpo_pb2.TrialConfig = hpo_pb2.TrialConfig() + for config in data: + tunable: hpo_pb2.TunableConfig = hpo_pb2.TunableConfig() + tunable.name = config['tunable_name'] + tunable.value = config['tunable_value'] + trialConfig.config.extend([tunable]) + + context.set_code(grpc.StatusCode.OK) + return trialConfig + else: + context.set_code(grpc.StatusCode.NOT_FOUND) + context.set_details('Could not find experiment: %s' % request.experiment_name) + return hpo_pb2.TunableConfig() + + def UpdateTrialResult(self, request, context): + if (hpo_service.instance.containsExperiment(request.experiment_name) and + request.trial == hpo_service.instance.get_trial_number(request.experiment_name)): + hpo_service.instance.set_result(request.experiment_name, + request.result, + request.value_type, + request.value) + context.set_code(grpc.StatusCode.OK) + return hpo_pb2.ExperimentTrialReply() + else: + context.set_code(grpc.StatusCode.NOT_FOUND) + context.set_details('Experiment not found or invalid trial number!') + return hpo_pb2.ExperimentTrialReply() + + def GenerateNextConfig(self, request, context): + trial_number = hpo_service.instance.get_trial_number(request.experiment_name) + reply : NewExperimentsReply = NewExperimentsReply() + reply.trial_number = trial_number + context.set_code(grpc.StatusCode.OK) + return reply + +def serve(): + server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + hpo_pb2_grpc.add_HpoServiceServicer_to_server(HpoService(), server) + server.add_insecure_port(host_name + ':' + str(server_port)) + print("Starting gRPC server at http://%s:%s" % (host_name, server_port)) + + server.start() + server.wait_for_termination() + + +if __name__ == '__main__': + logging.basicConfig() + serve() diff --git a/src/hpo_service.py b/src/hpo_service.py index c2ecb52..7bf1d78 100644 --- a/src/hpo_service.py +++ b/src/hpo_service.py @@ -1,6 +1,23 @@ +""" +Copyright (c) 2020, 2022 Red Hat, IBM Corporation and others. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + import threading import json from bayes_optuna import optuna_hpo +from exceptions import ExperimentNotFoundError class HpoService: """ @@ -38,10 +55,13 @@ def containsExperiment(self, name): def doesNotContainExperiment(self, name): return not self.containsExperiment(name) + def getExperimentsList(self): + return self.experiments.keys() + def getExperiment(self, name) -> optuna_hpo.HpoExperiment: if self.doesNotContainExperiment(name): - print("Experiment does not exist") - return + print("Experiment " + name + " does not exist") + raise ExperimentNotFoundError return self.experiments.get(name) @@ -65,10 +85,9 @@ def get_trial_json_object(self, id_): if experiment.hpo_algo_impl in ("optuna_tpe", "optuna_tpe_multivariate", "optuna_skopt"): try: experiment.resultsAvailableCond.acquire() - trial_json_object = json.dumps(experiment.trialDetails.trial_json_object) + return json.dumps(experiment.trialDetails.trial_json_object) finally: experiment.resultsAvailableCond.release() - return trial_json_object def set_result(self, id_, trial_result, result_value_type, result_value): diff --git a/src/rest_service.py b/src/rest_service.py new file mode 100644 index 0000000..b8617f8 --- /dev/null +++ b/src/rest_service.py @@ -0,0 +1,182 @@ +""" +Copyright (c) 2020, 2022 Red Hat, IBM Corporation and others. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from http.server import BaseHTTPRequestHandler, HTTPServer +import re +import cgi +import json +import requests +import os +from urllib.parse import urlparse, parse_qs + +from json_validate import validate_trial_generate_json +from tunables import get_all_tunables +from logger import get_logger + +import hpo_service + +logger = get_logger(__name__) + +n_trials = 10 +n_jobs = 1 +autotune_object_ids = {} +search_space_json = [] + +api_endpoint = "/experiment_trials" +host_name="0.0.0.0" +server_port = 8085 + +fileDir = os.path.dirname(os.path.realpath('index.html')) +filename = os.path.join(fileDir, 'index.html') +welcome_page=filename + + +class HTTPRequestHandler(BaseHTTPRequestHandler): + """ + A class used to handle the HTTP requests that arrive at the server. + + The handler will parse the request and the headers, then call a method specific to the request type. The method name + is constructed from the request. For example, for the request method GET, the do_GET() method will be called. + """ + + def _set_response(self, status_code, return_value): + # TODO: add status_message + self.send_response(status_code) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(return_value.encode('utf-8')) + + def do_POST(self): + """Serve a POST request.""" + if re.search(api_endpoint + "$", self.path): + content_type, params = cgi.parse_header(self.headers.get('content-type')) + if content_type == 'application/json': + length = int(self.headers.get('content-length')) + str_object = self.rfile.read(length).decode('utf8') + json_object = json.loads(str_object) + # TODO: validate structure of json_object for each operation + if json_object["operation"] == "EXP_TRIAL_GENERATE_NEW": + self.handle_generate_new_operation(json_object) + elif json_object["operation"] == "EXP_TRIAL_GENERATE_SUBSEQUENT": + self.handle_generate_subsequent_operation(json_object) + elif json_object["operation"] == "EXP_TRIAL_RESULT": + self.handle_result_operation(json_object) + else: + self._set_response(400, "-1") + else: + self._set_response(400, "-1") + else: + self._set_response(403, "-1") + + def do_GET(self): + """Serve a GET request.""" + if re.search(api_endpoint, self.path): + query = parse_qs(urlparse(self.path).query) + + if ("experiment_name" in query and "trial_number" in query and hpo_service.instance.containsExperiment(query["experiment_name"][0]) and + query["trial_number"][0] == str(hpo_service.instance.get_trial_number(query["experiment_name"][0]))): + data = hpo_service.instance.get_trial_json_object(query["experiment_name"][0]) + self._set_response(200, data) + else: + self._set_response(404, "-1") + elif (self.path == "/"): + data = self.getHomeScreen() + self._set_response(200, data) + else: + self._set_response(403, "-1") + + def getHomeScreen(self): + fin = open(welcome_page) + content = fin.read() + fin.close() + return content + + def handle_generate_new_operation(self, json_object): + """Process EXP_TRIAL_GENERATE_NEW operation.""" + is_valid_json_object = validate_trial_generate_json(json_object) + + if is_valid_json_object and hpo_service.instance.doesNotContainExperiment(json_object["search_space"]["experiment_name"]): + search_space_json = json_object["search_space"] + if str(search_space_json["experiment_name"]).isspace() or not str(search_space_json["experiment_name"]): + self._set_response(400, "-1") + return + get_search_create_study(search_space_json, json_object["operation"]) + trial_number = hpo_service.instance.get_trial_number(json_object["search_space"]["experiment_name"]) + self._set_response(200, str(trial_number)) + else: + self._set_response(400, "-1") + + def handle_generate_subsequent_operation(self, json_object): + """Process EXP_TRIAL_GENERATE_SUBSEQUENT operation.""" + is_valid_json_object = validate_trial_generate_json(json_object) + experiment_name = json_object["experiment_name"] + if is_valid_json_object and hpo_service.instance.containsExperiment(experiment_name): + trial_number = hpo_service.instance.get_trial_number(experiment_name) + self._set_response(200, str(trial_number)) + else: + self._set_response(400, "-1") + + def handle_result_operation(self, json_object): + """Process EXP_TRIAL_RESULT operation.""" + if (hpo_service.instance.containsExperiment(json_object["experiment_name"]) and + json_object["trial_number"] == hpo_service.instance.get_trial_number(json_object["experiment_name"])): + hpo_service.instance.set_result(json_object["experiment_name"], json_object["trial_result"], json_object["result_value_type"], + json_object["result_value"]) + self._set_response(200, "0") + else: + self._set_response(400, "-1") + + +def get_search_create_study(search_space_json, operation): + # TODO: validate structure of search_space_json + + if operation == "EXP_TRIAL_GENERATE_NEW": + if "parallel_trials" not in search_space_json: + search_space_json["parallel_trials"] = n_jobs + experiment_name, total_trials, parallel_trials, direction, hpo_algo_impl, id_, objective_function, tunables, value_type = get_all_tunables( + search_space_json) + if (not parallel_trials): + parallel_trials = n_jobs + elif parallel_trials != 1: + raise Exception("Parallel Trials value should be '1' only!") + + logger.info("Total Trials = "+str(total_trials)) + logger.info("Parallel Trials = "+str(parallel_trials)) + + if hpo_algo_impl in ("optuna_tpe", "optuna_tpe_multivariate", "optuna_skopt"): + hpo_service.instance.newExperiment(id_, experiment_name, total_trials, parallel_trials, direction, hpo_algo_impl, objective_function, + tunables, value_type) + print("Starting Experiment: " + experiment_name) + hpo_service.instance.startExperiment(experiment_name) + + +def get_search_space(id_, url): + """Perform a GET request and return the search space json.""" + params = {"id": id_} + r = requests.get(url, params) + r.raise_for_status() + search_space_json = r.json() + return search_space_json + + + +def main(): + server = HTTPServer((host_name, server_port), HTTPRequestHandler) + logger.info("Access server at http://%s:%s" % ("localhost", server_port)) + server.serve_forever() + +if __name__ == '__main__': + main() diff --git a/src/service.py b/src/service.py index 37918ba..ec63312 100644 --- a/src/service.py +++ b/src/service.py @@ -14,169 +14,18 @@ limitations under the License. """ -from http.server import BaseHTTPRequestHandler, HTTPServer -import re -import cgi -import json -import requests -import os -from urllib.parse import urlparse, parse_qs +import rest_service, grpc_service +import threading -from json_validate import validate_trial_generate_json -from tunables import get_all_tunables -from logger import get_logger +def main(): + restService = threading.Thread(target=rest_service.main) + gRPCservice = threading.Thread(target=grpc_service.serve) -import hpo_service + restService.start() + gRPCservice.start() -logger = get_logger(__name__) - -n_trials = 10 -n_jobs = 1 -autotune_object_ids = {} -search_space_json = [] - -api_endpoint = "/experiment_trials" -host_name="0.0.0.0" -server_port = 8085 - -fileDir = os.path.dirname(os.path.realpath('index.html')) -filename = os.path.join(fileDir, 'index.html') -welcome_page=filename - - -class HTTPRequestHandler(BaseHTTPRequestHandler): - """ - A class used to handle the HTTP requests that arrive at the server. - - The handler will parse the request and the headers, then call a method specific to the request type. The method name - is constructed from the request. For example, for the request method GET, the do_GET() method will be called. - """ - - def _set_response(self, status_code, return_value): - # TODO: add status_message - self.send_response(status_code) - self.send_header('Content-type', 'text/html') - self.end_headers() - self.wfile.write(return_value.encode('utf-8')) - - def do_POST(self): - """Serve a POST request.""" - if re.search(api_endpoint + "$", self.path): - content_type, params = cgi.parse_header(self.headers.get('content-type')) - if content_type == 'application/json': - length = int(self.headers.get('content-length')) - str_object = self.rfile.read(length).decode('utf8') - json_object = json.loads(str_object) - # TODO: validate structure of json_object for each operation - if json_object["operation"] == "EXP_TRIAL_GENERATE_NEW": - self.handle_generate_new_operation(json_object) - elif json_object["operation"] == "EXP_TRIAL_GENERATE_SUBSEQUENT": - self.handle_generate_subsequent_operation(json_object) - elif json_object["operation"] == "EXP_TRIAL_RESULT": - self.handle_result_operation(json_object) - else: - self._set_response(400, "-1") - else: - self._set_response(400, "-1") - else: - self._set_response(403, "-1") - - def do_GET(self): - """Serve a GET request.""" - if re.search(api_endpoint, self.path): - query = parse_qs(urlparse(self.path).query) - - if ("experiment_name" in query and "trial_number" in query and hpo_service.instance.containsExperiment(query["experiment_name"][0]) and - query["trial_number"][0] == str(hpo_service.instance.get_trial_number(query["experiment_name"][0]))): - data = hpo_service.instance.get_trial_json_object(query["experiment_name"][0]) - self._set_response(200, data) - else: - self._set_response(404, "-1") - elif (self.path == "/"): - data = self.getHomeScreen() - self._set_response(200, data) - else: - self._set_response(403, "-1") - - def getHomeScreen(self): - fin = open(welcome_page) - content = fin.read() - fin.close() - return content - - def handle_generate_new_operation(self, json_object): - """Process EXP_TRIAL_GENERATE_NEW operation.""" - is_valid_json_object = validate_trial_generate_json(json_object) - - if is_valid_json_object and hpo_service.instance.doesNotContainExperiment(json_object["search_space"]["experiment_name"]): - search_space_json = json_object["search_space"] - if str(search_space_json["experiment_name"]).isspace() or not str(search_space_json["experiment_name"]): - self._set_response(400, "-1") - return - get_search_create_study(search_space_json, json_object["operation"]) - trial_number = hpo_service.instance.get_trial_number(json_object["search_space"]["experiment_name"]) - self._set_response(200, str(trial_number)) - else: - self._set_response(400, "-1") - - def handle_generate_subsequent_operation(self, json_object): - """Process EXP_TRIAL_GENERATE_SUBSEQUENT operation.""" - is_valid_json_object = validate_trial_generate_json(json_object) - experiment_name = json_object["experiment_name"] - if is_valid_json_object and hpo_service.instance.containsExperiment(experiment_name): - trial_number = hpo_service.instance.get_trial_number(experiment_name) - self._set_response(200, str(trial_number)) - else: - self._set_response(400, "-1") - - def handle_result_operation(self, json_object): - """Process EXP_TRIAL_RESULT operation.""" - if (hpo_service.instance.containsExperiment(json_object["experiment_name"]) and - json_object["trial_number"] == hpo_service.instance.get_trial_number(json_object["experiment_name"])): - hpo_service.instance.set_result(json_object["experiment_name"], json_object["trial_result"], json_object["result_value_type"], - json_object["result_value"]) - self._set_response(200, "0") - else: - self._set_response(400, "-1") - - -def get_search_create_study(search_space_json, operation): - # TODO: validate structure of search_space_json - - if operation == "EXP_TRIAL_GENERATE_NEW": - if "parallel_trials" not in search_space_json: - search_space_json["parallel_trials"] = n_jobs - experiment_name, total_trials, parallel_trials, direction, hpo_algo_impl, id_, objective_function, tunables, value_type = get_all_tunables( - search_space_json) - if (not parallel_trials): - parallel_trials = n_jobs - elif parallel_trials != 1: - raise Exception("Parallel Trials value should be '1' only!") - - logger.info("Total Trials = "+str(total_trials)) - logger.info("Parallel Trials = "+str(parallel_trials)) - - if hpo_algo_impl in ("optuna_tpe", "optuna_tpe_multivariate", "optuna_skopt"): - hpo_service.instance.newExperiment(id_, experiment_name, total_trials, parallel_trials, direction, hpo_algo_impl, objective_function, - tunables, value_type) - print("Starting Experiment: " + experiment_name) - hpo_service.instance.startExperiment(experiment_name) - - -def get_search_space(id_, url): - """Perform a GET request and return the search space json.""" - params = {"id": id_} - r = requests.get(url, params) - r.raise_for_status() - search_space_json = r.json() - return search_space_json - - - -def main(): - server = HTTPServer((host_name, server_port), HTTPRequestHandler) - logger.info("Access server at http://%s:%s" % ("localhost", server_port)) - server.serve_forever() + restService.join() + gRPCservice.join() if __name__ == '__main__': main() diff --git a/tests/resources/searchspace_jsons/newExperiment.json b/tests/resources/searchspace_jsons/newExperiment.json new file mode 100644 index 0000000..d115fa9 --- /dev/null +++ b/tests/resources/searchspace_jsons/newExperiment.json @@ -0,0 +1,26 @@ +{ + "experiment_name": "petclinic-sample-2-75884c5549-npvgd", + "total_trials": 100, + "parallel_trials": 1, + "value_type": "double", + "hpo_algo_impl": "optuna_tpe", + "objective_function": "transaction_response_time", + "tuneables": [ + { + "value_type": "double", + "lower_bound": 150, + "name": "memoryRequest", + "upper_bound": 300, + "step": 1 + }, + { + "value_type": "double", + "lower_bound": 1.0, + "name": "cpuRequest", + "upper_bound": 3.0, + "step": 0.01 + } + ], + "slo_class": "response_time", + "direction": "minimize" +}