Jack Norman
04/10/2024, 1:37 AMservice.py I create my service by passing in my ~15 runners. Before I pass the runners in, I first load them into memory. But what I’m seeing is that the runners are being loaded in something like 50+ times each. Because each runner is large, I get memory issues right away. Has anyone seen this issue before? Is there something upstream that runs service.py in a loop perhaps?Jian Shen Yap
04/10/2024, 6:39 AMJack Norman
04/10/2024, 6:37 PMimport bentoml
import pandas as pd
from constants import BUCKET
class Pipeline1:
def __init__(self, bento_models, runners):
self.bento_models = bento_models
self.runners = runners
self.model_name = 'model1'
self.custom_objects = bento_models[self.model_name].custom_objects
self.nums = self.custom_objects['nums']
self.cats = self.custom_objects['cats']
self.encoder = self.custom_objects['encoder']
self.model_version = self.custom_objects['model_version']
self.feature_names = list(self.custom_objects['feature_names'])
self.classes = ['negative', 'positive']
def prepare_data(self, input_data):
input_data = input_data[self.nums + self.cats]
return self.custom_objects['prepare_data'](input_data, self.cats, self.nums, '.', self.encoder)
def predict_proba(self, input_data):
data = self.prepare_data(input_data)
results = self.runners[self.model_name].predict_proba.run(data)
results_df = pd.DataFrame(results, columns=self.classes)
return results_df
def load_bentos_from_gcs(bento_framework_model_names, load_from_gcs=True):
bento_models = {}
runners = {}
for model_tmp in bento_framework_model_names:
model_name_tmp = model_tmp["model_name"]
framework = model_tmp["framework"]
if load_from_gcs:
bentoml.models.import_model(f"{BUCKET}/{model_name_tmp}.bentomodel")
if framework == "sklearn":
bento_model = bentoml.sklearn.get(f"{model_name_tmp}:latest")
elif framework == "picklable_model":
bento_model = bentoml.picklable_model.get(f"{model_name_tmp}:latest")
else:
pass # handle other cases
runner = bento_model.to_runner()
bento_models[model_name_tmp] = bento_model
runners[model_name_tmp] = runner
return bento_models, runners
bento_models, runners = load_bentos_from_gcs(BENTO_FRAMEWORK_MODEL_NAMES, load_from_gcs=False)
svc = bentoml.Service("probability-models-bento", runners=list(runners.values()))
pipeline1 = Pipeline1(bento_models, runners)
@svc.api(input=input_descriptor, output=output_descriptor, route=f"{pipeline1.model_name}/predict_proba")
def pipeline1_predict_proba(input_data: InputDescriptor) -> OutputDescriptor:
validated_results = pipeline1.predict_proba(input_data)
return validated_resultsJack Norman
04/10/2024, 6:39 PMload_bentos_from_gcs() is being called hundreds of times which means that the Bento models are being loaded in hundreds of timesJack Norman
04/10/2024, 6:40 PMlen(BENTO_FRAMEWORK_MODEL_NAMES) is 14Jack Norman
04/10/2024, 6:40 PMJack Norman
04/10/2024, 6:42 PMJack Norman
04/10/2024, 6:42 PMJack Norman
04/10/2024, 6:46 PMload_bentos_from_gcs() is I think something like api_workers x num_runners x num_runners + num_runnersJian Shen Yap
04/10/2024, 6:50 PMpipeline1 = Pipeline1(bento_models, runners)
^ i think it comes from here.
each Pipeline object contains all runner and is being referenced directly into @svc which creates a process for each api workers, which explain why you have a surge of memory.Jian Shen Yap
04/10/2024, 6:51 PMJian Shen Yap
04/10/2024, 6:54 PMJack Norman
04/10/2024, 6:57 PMpipeline1_predict_proba) calls pipeline1.predict_proba(input_data) which uses the runner self.runners[self.model_name].predict_proba.run(data)Jack Norman
04/10/2024, 6:57 PMJack Norman
04/10/2024, 7:00 PMJack Norman
04/11/2024, 4:53 PMJack Norman
04/13/2024, 12:31 AMbento_models, runners = load_bentos_from_gcs(BENTO_FRAMEWORK_MODEL_NAMES, load_from_gcs=False)
svc = bentoml.Service("probability-models-bento", runners=list(runners.values()))
name = "mymodel"
model = bento_models[name]
runner = runners[name]
pipeline1 = Pipeline1(model, runner)
@svc.api(input=input_descriptor, output=output_descriptor, route=f"{pipeline1.model_name}/predict_proba")
def pipeline1_predict_proba(input_data: InputDescriptor) -> OutputDescriptor:
validated_results = pipeline1.predict_proba(input_data)
return validated_results
I also commented out all other @svc.api endpoints so that there’s just this one. And I’m still getting the huge surge of memory. Is this because I’m passing all the runners into the Service? How else do you recommend I do it? The runners are not that big of objects, ranging from 50 MB to 1.5 GB.Jack Norman
04/14/2024, 5:19 PM# service.py
bento_models, runners = load_bento_models(BENTO_FRAMEWORK_MODEL_NAMES, from_gcs=False) # return (14 models, 14 runners)
name = "pipeline1"
model = bento_models[name]
runner = runners[name]
svc = bentoml.Service("probability-models-bento", runners=[runner]) # only pass in one runner, for testing purposes
@svc.api(input=input_descriptor, output=output_descriptor, route=f"{pipeline1.model_name}/predict_proba")
def pipeline1_predict_proba(input_data: InputDescriptor) -> OutputDescriptor:
results = runner.predict_proba.run(input_data)
return results
Now what I’m seeing is that
1. all 14 runners are loaded for each api worker
2. all 14 runners are loaded for each of the runners (of which there is only one in this simplified example) that is passed into the service.
#2 would suggest that service.py gets run for each runner passed into Service(). This would be why when I pass in the list of all runners into Service(), I get more than 14*14 model/runner loads and the memory explodes. How do I load my models and runners but only the ones that are relevant to each runner?Jack Norman
04/16/2024, 4:21 PMload_bento_models function does is it loads all 14 of the models from the BentoML store using bentoml.sklearn.get(f"{model_name_tmp}:latest") and then it uses .to_runner() to convert to a runner.Jack Norman
04/16/2024, 4:34 PMload_bento_models in order to load in the models from the Bentoml store.
• Observation: It seems as though service.py gets run for cli, each api server, and for each runner.
• Observation: Because service.py gets run for each runner, and each run loads in all models from the Bentoml store, I end up loading the models more than 14^2 times.
• QuestIon: How do I load the models in from the Bentoml store so that I can pass them into Service() without having this n^2 behavior?Jian Shen Yap
04/16/2024, 4:40 PMJack Norman
04/16/2024, 4:43 PMJian Shen Yap
04/16/2024, 4:45 PMJack Norman
04/16/2024, 4:46 PMJian Shen Yap
04/16/2024, 4:54 PMNow what I’m seeing is that
1. all 14 runners are loaded for each api workerThis is not the case. Runners by default are only loaded once. Here's an example
# %%
# ########################################
# 🍀🍀🍀🍀🍀🍀🍀🍀 Vanilla Text Runner
# ########################################
import sys
import numpy as np
import bentoml
class TextRunnable(bentoml.Runnable):
SUPPORTED_RESOURCES = ("cpu",)
SUPPORTS_CPU_MULTI_THREADING = True
def __init__(self):
print("runner")
@bentoml.Runnable.method()
def generate(self, size_in_bytes: int) -> np.ndarray:
# generate a payload of varying size of ndarray
payload = np.random.rand(1, 1, int(size_in_bytes[0]))
print(f"Size of payload = {sys.getsizeof(payload)}")
return payload
text_runner = bentoml.Runner(TextRunnable)
# sse_runner = bentoml.Runner(SSERunnable)
svc = bentoml.Service("text_runner", runners=[text_runner])
@svc.api(input=bentoml.io.Text(), output=bentoml.io.NumpyNdarray())
async def count(size_in_bytes:str) -> str:
size = int(size_in_bytes)
ret = await text_runner.async_run(size_in_bytes*size)
return ret
my CPU has 24 core, so it spins up 24 api workers but only 1 runnerJian Shen Yap
04/16/2024, 4:56 PMload_bento_models in service.py so it loads all the model for each api worker, not the runnerJack Norman
04/16/2024, 4:59 PMservice.py?Jack Norman
04/16/2024, 5:00 PMload_bento_models function is doing?Jian Shen Yap
04/16/2024, 5:03 PMservice.pyJian Shen Yap
04/16/2024, 5:04 PM# %%
########################################
# 🤖🤖🤖🤖🤖🤖🤖🤖 Transformer Service
########################################
import bentoml
print("hello")
summarizer_runner = bentoml.models.get("summarization:4r5m6nsrnwecfbo7").to_runner()
svc = bentoml.Service(
name="summarization", runners=[summarizer_runner]
)
@svc.api(input=bentoml.io.Text(), output=bentoml.io.Text())
async def summarize(text: str) -> str:
generated = await summarizer_runner.async_run(text, max_length=3000)
return generated[0]["summary_text"]
you will see hello printed many times, but there will only be 1 runner process if you do ps aux | grep pythonJian Shen Yap
04/16/2024, 5:04 PMJack Norman
04/16/2024, 5:05 PMJian Shen Yap
04/16/2024, 5:06 PMJack Norman
04/16/2024, 5:07 PMsummarizer_runner1 = bentoml.models.get("summarization:asdfasdf1").to_runner()
summarizer_runner2 = bentoml.models.get("summarization:asdfasdf2").to_runner()
...
svc = bentoml.Service(
name="summarization", runners=[summarizer_runner1, summarizer_runner2]
)
Then I think you would see that each of summarizer_runner{n} would get loaded in for each of the runners you pass into Service()Jack Norman
04/16/2024, 5:07 PM14^2 timesJian Shen Yap
04/16/2024, 5:13 PMbentoml.models.get("summarization:asdfasdf1").to_runner()
This does not start the runner itself but merely setting up the metadata etc.Jian Shen Yap
04/16/2024, 5:19 PMRunnable initJack Norman
04/16/2024, 5:21 PMRunnable init, which gets run at what point? .to_runner()?Jian Shen Yap
04/16/2024, 5:23 PM.to_runner() -> gives a Runner
Runnable is an abstraction that represent the runner process that is spun up. this is an internal bentoml abstractionJian Shen Yap
04/16/2024, 5:25 PMRunnable happens somewhere when we call bentoml serveJack Norman
04/16/2024, 5:27 PMload_bento_models shouldn’t be problematic because it’s just calling bentoml.models.get and .to_runner() 14 times for each of the 14 runners passed into Service(), but the serve should have it actually be loaded into memory just once (each)?Jack Norman
04/16/2024, 5:28 PMload_bento_models is any different from the setup you have in your service.pyJian Shen Yap
04/16/2024, 5:29 PMJian Shen Yap
04/16/2024, 5:29 PMbento_models, runners = load_bento_models(BENTO_FRAMEWORK_MODEL_NAMES, from_gcs=False) # return (14 models, 14 runners)
name = "pipeline1"
model = bento_models[name]
runner = runners[name]
svc = bentoml.Service("probability-models-bento", runners=[runner]) # only pass in one runner, for testing purposes
in a python shellJian Shen Yap
04/16/2024, 5:29 PMJian Shen Yap
04/16/2024, 5:29 PMJack Norman
04/16/2024, 6:12 PMtop) goes from ~4030 to ~4661. When I run it in a for loop and run it 14*14 times, it goes from ~3987 MiB Mem used to ~6223 MiB Mem used. So it does build up memory usage but you’re right that it’s not as much as 14*14*(4661 - 4030)Jack Norman
04/17/2024, 4:51 PMcustom_objects when I’m defining my endpoint logic. Are the model objects also only loaded into memory just once or would they be loaded into memory 14*14 times?Jian Shen Yap
04/17/2024, 4:53 PMJian Shen Yap
04/17/2024, 4:53 PMJack Norman
04/17/2024, 10:19 PMdocker run -it --rm -p 3000:3000 probability-models-bento:vbeq37x47oc4mqqb serve --production --api-workers 2 which uses more a lot more memory than when I open up a Python shell and load in the Service many, many times. This is at about 28GiBs memory used and counting, when I start to get “Cannot allocate memory” errors. Do you have any other ideas?