Skip to content

Commit

Permalink
feat: gRPC client implementation
Browse files Browse the repository at this point in the history
Signed-off-by: Aaron Pham <29749331+aarnphm@users.noreply.github.com>
Signed-off-by: Ubuntu <29749331+aarnphm@users.noreply.github.com>
  • Loading branch information
aarnphm committed Nov 29, 2022
1 parent 85826bf commit f5eda45
Show file tree
Hide file tree
Showing 12 changed files with 1,004 additions and 101 deletions.
57 changes: 34 additions & 23 deletions grpc-client/python/client.py
@@ -1,32 +1,43 @@
from __future__ import annotations

import asyncio

import grpc
import numpy as np

from bentoml.client import Client


from bentoml.grpc.utils import import_generated_stubs
async def arun(client: Client):

pb, services = import_generated_stubs()
res = await client.async_classify(np.array([[5.9, 3, 5.1, 1.8]]))
print("Result from 'client.async_classify':\n", res)
res = await client.async_call("classify", np.array([[5.9, 3, 5.1, 1.8]]))
print("Result from 'client.async_call':\n", res)


async def run():
async with grpc.aio.insecure_channel("localhost:3000") as channel:
stub = services.BentoServiceStub(channel)
req = await stub.Call(
request=pb.Request(
api_name="classify",
ndarray=pb.NDArray(
dtype=pb.NDArray.DTYPE_FLOAT,
shape=(1, 4),
float_values=[5.9, 3, 5.1, 1.8],
),
)
)
print(req)
def run(client: Client):
res = client.classify(np.array([[5.9, 3, 5.1, 1.8]]))
print("Result from 'client.classify':\n", res)
res = client.call("classify", np.array([[5.9, 3, 5.1, 1.8]]))
print("Result from 'client.call(bentoml_api_name='classify')':\n", res)


if __name__ == "__main__":
loop = asyncio.new_event_loop()
try:
loop.run_until_complete(run())
finally:
loop.close()
assert loop.is_closed()
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("-rwa", "--run-with-async", action="store_true", default=False)
parser.add_argument("--grpc", action="store_true", default=False)
args = parser.parse_args()

c = Client.from_url("localhost:3000", grpc=args.grpc)

if args.run_with_async:
loop = asyncio.new_event_loop()
try:
loop.run_until_complete(arun(c))
finally:
loop.close()
assert loop.is_closed()
else:
run(c)
14 changes: 6 additions & 8 deletions pyproject.toml
Expand Up @@ -136,17 +136,15 @@ grpc = [
# Restrict maximum version due to breaking protobuf 4.21.0 changes
# (see https://github.com/protocolbuffers/protobuf/issues/10051)
# 3.19.5 is currently breaking on a lot of system.
"protobuf>=3.5.0, <3.20, !=3.19.5",
"protobuf>=3.5.0,<4.0dev,!=3.19.5",
# Lowest version that support 3.10. We need to set an upper bound
# We can't use 1.48.2 since it depends on 3.19.5
"grpcio>=1.41.0,!=1.48.2",
# grpcio>=1.48.0 provides a pre-built M1 wheel.
"grpcio>=1.48.0,!=1.48.2;platform_machine=='arm64' and platform_system=='Darwin'",
"grpcio-health-checking>=1.41.0,!=1.48.2",
"grpcio>=1.41.0",
"grpcio-health-checking>=1.41.0",
"opentelemetry-instrumentation-grpc==0.35b0",
]
grpc-reflection = ["bentoml[grpc]", "grpcio-reflection>=1.41.0,!=1.48.2"]
grpc-channelz = ["bentoml[grpc]", "grpcio-channelz>=1.41.0,!=1.48.2"]
grpc-reflection = ["bentoml[grpc]", "grpcio-reflection>=1.41.0"]
grpc-channelz = ["bentoml[grpc]", "grpcio-channelz>=1.41.0"]
# We kept for compatibility with previous
# versions of BentoML. It is discouraged to use this, instead use any
# of the above tracing.* extras.
Expand Down Expand Up @@ -316,7 +314,7 @@ skip_glob = [
]

[tool.pyright]
pythonVersion = "3.10"
pythonVersion = "3.11"
include = ["src/", "examples/", "tests/"]
exclude = [
'src/bentoml/_version.py',
Expand Down
2 changes: 0 additions & 2 deletions requirements/tests-requirements.txt
Expand Up @@ -17,6 +17,4 @@ imageio==2.22.4
pyarrow==10.0.1
build[virtualenv]==0.9.0
protobuf==3.19.6
grpcio>=1.41.0, <1.49, !=1.48.2
grpcio-health-checking>=1.41.0, <1.49, !=1.48.2
opentelemetry-instrumentation-grpc==0.35b0
5 changes: 4 additions & 1 deletion scripts/generate_grpc_stubs.sh
@@ -1,15 +1,18 @@
#!/usr/bin/env bash

set -e

GIT_ROOT=$(git rev-parse --show-toplevel)
STUBS_GENERATOR="bentoml/stubs-generator"
BASEDIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" &>/dev/null && pwd 2>/dev/null)"

cd "$GIT_ROOT/src" || exit 1

main() {
local VERSION="${1:-v1}"
# Use inline heredoc for even faster build
# Keeping image as cache should be fine since we only want to generate the stubs.
if [[ $(docker images --filter=reference="$STUBS_GENERATOR" -q) == "" ]] || test "$(git diff --name-only --diff-filter=d -- "$0")"; then
if test "$(git diff --name-only --diff-filter=d -- "$BASEDIR/$(basename "$0")")" || [[ $(docker images --filter=reference="$STUBS_GENERATOR" -q) == "" ]]; then
docker buildx build --platform=linux/amd64 -t "$STUBS_GENERATOR" --load -f- . <<EOF
# syntax=docker/dockerfile:1.4-labs
Expand Down
4 changes: 2 additions & 2 deletions src/bentoml/_internal/io_descriptors/json.py
Expand Up @@ -240,11 +240,11 @@ def from_spec(cls, spec: SpecDict) -> Self:
if "args" not in spec:
raise InvalidArgument(f"Missing args key in JSON spec: {spec}")
if "has_pydantic_model" in spec["args"] and spec["args"]["has_pydantic_model"]:
logger.warning(
logger.debug(
"BentoML does not support loading pydantic models from URLs; output will be a normal dictionary."
)
if "has_json_encoder" in spec["args"] and spec["args"]["has_json_encoder"]:
logger.warning(
logger.debug(
"BentoML does not support loading JSON encoders from URLs; output will be a normal dictionary."
)

Expand Down
8 changes: 4 additions & 4 deletions src/bentoml/_internal/server/grpc/server.py
Expand Up @@ -48,7 +48,7 @@
)


def _load_from_file(p: str) -> bytes:
def load_from_file(p: str) -> bytes:
rp = resolve_user_filepath(p, ctx=None)
with open(rp, "rb") as f:
return f.read()
Expand Down Expand Up @@ -164,12 +164,12 @@ def configure_port(self, addr: str):
), "'ssl_keyfile' is required when 'ssl_certfile' is provided."
if self.ssl_ca_certs is not None:
client_auth = True
ca_cert = _load_from_file(self.ssl_ca_certs)
ca_cert = load_from_file(self.ssl_ca_certs)
server_credentials = grpc.ssl_server_credentials(
(
(
_load_from_file(self.ssl_keyfile),
_load_from_file(self.ssl_certfile),
load_from_file(self.ssl_keyfile),
load_from_file(self.ssl_certfile),
),
),
root_certificates=ca_cert,
Expand Down
7 changes: 5 additions & 2 deletions src/bentoml/_internal/server/grpc/servicer.py
Expand Up @@ -169,6 +169,7 @@ async def Call(
else:
output = await api.func(input_data)
else:
assert api.func is not None
if api.multi_input:
output = await anyio.to_thread.run_sync(api.func, **input_data)
else:
Expand Down Expand Up @@ -212,10 +213,12 @@ async def ServiceMetadata(
name=api.name,
docs=api.doc,
input=make_descriptor_spec(
api.input.to_spec(), ServiceMetadataResponse
t.cast("SpecDict", api.input.to_spec()),
ServiceMetadataResponse,
),
output=make_descriptor_spec(
api.output.to_spec(), ServiceMetadataResponse
t.cast("SpecDict", api.output.to_spec()),
ServiceMetadataResponse,
),
)
for api in service.apis.values()
Expand Down
2 changes: 1 addition & 1 deletion src/bentoml/_internal/service/inference_api.py
Expand Up @@ -26,7 +26,7 @@
class InferenceAPI:
def __init__(
self,
user_defined_callback: t.Callable[..., t.Any],
user_defined_callback: t.Callable[..., t.Any] | None,
input_descriptor: IODescriptor[t.Any],
output_descriptor: IODescriptor[t.Any],
name: Optional[str],
Expand Down

0 comments on commit f5eda45

Please sign in to comment.