Create a BentoML project
A Bento is the artifact bentoml build produces; a project is the directory it is
built from, normally one service.py plus its dependencies. @bentoml.service on a class
makes it a Service — the unit that gets deployed and scaled, and there may be several in
one bento — and @bentoml.api on a method makes it an HTTP endpoint whose schema is the
method's type annotations. BentoML supplies the server, the OpenAPI spec, the Swagger UI,
the client, health checks and metrics. You write the class.
Two paths, same steps 2-5: A from scratch (§1A) · B convert existing code (§1B).
It also covers an existing project: adding an endpoint, switching an API to streaming or
adaptive batching, moving long work to a background task, splitting one Service into several,
and diagnosing a bento that will not start, rejects valid input, or serves one request at a
time (last section).
What you produce
| File |
When |
Purpose |
service.py |
always |
The Services and their APIs. The whole surface. |
requirements.txt |
always |
Runtime deps, referenced by the Image and pip-installed locally |
.bentoignore |
always |
Keeps data, venvs, checkpoints, __pycache__ out of the bento |
test_service.py |
always |
The anchor case as a test (§4) |
save_model.py |
own weights |
One-shot script that puts weights in the model store |
bentofile.yaml / pyproject.toml |
legacy only |
Alternative to Image; use Image instead (build options) |
No Dockerfile, no uvicorn/gunicorn call, no main(), no route table, no request/response
plumbing. If you are writing any of those, you are working against the framework.
Step 0 — get the anchor case
Before writing anything, get one concrete input and its expected output from the user (or
from the existing code's tests/README). This is the anchor: §4 verifies against it. Without it
you can only confirm that a bento returns something, which is how a wrong one ships.
If the user cannot supply one, derive it from the model card / existing code and state the
assumption explicitly.
Step 1A — new project: ask once
Ask these together, with defaults, in one round. Accept "you decide" for any of them.
| Ask |
Default when the user has no preference |
| What does it do, and is it one endpoint or several? |
One, named after the verb (summarize, classify) |
| Where do the weights come from: a Hugging Face ID, local files, an existing model-store tag, or no model at all? |
HF ID if the user named a model; else no model |
| Input and output types, per endpoint |
str -> str, or the model's natural pair (§ io-types) |
| GPU required? |
No |
| Python version and pinned packages? |
Your current Python; unpinned packages |
| Response profile: interactive, long-running, token-streaming, or fire-and-forget? |
Interactive |
Do not ask about workers, threads, replicas, ports, batching, registries or timeouts. The
defaults are correct until something is measured, and every one of them is a one-line change
later.
Then scaffold the file set above and go to §2. Keep the first version to one Service and one
API even if the target is bigger — get it green, then grow.
Step 1B — convert existing code
Read the existing code first. Find and write down: (1) the inference entry point, (2) where the
weights live, (3) the real dependency list, (4) the anchor case. Then translate:
| In the existing code |
In the Bento |
Module-level model load, or a load_model() called per request |
__init__ — runs once per worker |
A pickle / .pt / directory of weights |
save_model.py once, then a class attribute BentoModel("name:latest") |
from_pretrained("org/model") |
class attribute HuggingFaceModel("org/model"), pass the returned path |
@app.post("/predict") handler |
@bentoml.api def predict(...) -> ... with annotations |
| Pydantic request/response models |
Keep them verbatim — use as a parameter type, or @bentoml.api(input_spec=Model) |
argparse / if __name__ == "__main__" |
Delete. bentoml serve is the entry point |
uvicorn.run(...), gunicorn worker count |
Delete; @bentoml.service(workers=N) |
Dockerfile, pip install lines |
bentoml.images.Image(...) (§3) |
| Custom routes, static files, webhooks, a UI |
Keep the FastAPI app as-is and mount it under a prefix: @bentoml.asgi_app(app, path="/v1") |
| Auth / rate limiting as a route dependency |
ASGI middleware (Service.add_asgi_middleware) — FastAPI Depends never sees @bentoml.api routes |
| A Flask/Django (WSGI) app |
Port the inference route to @bentoml.api; a WSGI app needs an ASGI adapter to mount |
BentoML 1.1 Runner / .to_runner() |
A second @bentoml.service + bentoml.depends(); bentoml.runner_service(runner) is a stopgap |
| Celery / RQ / a background thread |
@bentoml.task (§ recipes) |
| A notebook |
Cells to __init__ (setup) and one @bentoml.api (the inference cell) |
Rules for a conversion: do not rewrite the model code. Import it, call it, keep it
importable so the old and new paths can be diffed. Convert the smallest slice that serves the
anchor case, verify parity (§4), then move the rest. Detail and per-source playbooks:
conversion.
Step 2 — write service.py
from __future__ import annotations
import bentoml
from bentoml.models import HuggingFaceModel
with bentoml.importing(): # heavy/optional imports: skipped at build, required at serve
from transformers import pipeline
@bentoml.service(
resources={"cpu": "2"}, # deployment hint (see rules), not a local limit
traffic={"timeout": 30}, # seconds; default 60
)
class Summarization:
model_path = HuggingFaceModel("sshleifer/distilbart-cnn-12-6") # class scope, always
def __init__(self) -> None: # once per worker
self.pipeline = pipeline("summarization", model=self.model_path)
@bentoml.api # -> POST /summarize
def summarize(self, text: str) -> str:
return self.pipeline(text)[0]["summary_text"]
Rules that are not guessable from the API surface:
| Rule |
Why it bites |
Model references (HuggingFaceModel, BentoModel) go at class scope, not in __init__ |
Class scope is what declares the model a dependency of the bento. Inside __init__ it is not packaged, and deployment fails with model NotFound |
| Annotate every parameter and the return |
The annotations are the schema, the OpenAPI spec and the client. An unannotated parameter becomes Any: no validation, no docs, no client typing |
Optional[X] requires an explicit default (= None); other unions are unsupported |
Silent schema surprise otherwise |
Load models in __init__, never inside an API method |
Per-request loading is the single most common cause of "BentoML is slow" |
Service name = class name; @bentoml.service(name="...") overrides. API route = /<method_name>; @bentoml.api(route=...) overrides |
Deploy configs, BENTOML_SERVE_DEPENDS and clients all key off these names |
A method name may not start with __ |
Raises at import |
A sync API runs in a thread pool of size threads (default 1) — one request at a time per worker |
Concurrency comes from async def, or threads=N, or workers=N. Not from adding replicas |
Never block inside an async def API |
It stalls the event loop; /readyz starts failing under load. Use await self.dep.to_async.method(...), or anyio.to_thread.run_sync |
resources={"cpu": ..., "memory": ...} is a hint for the deployment platform — bentoml serve does not enforce it. resources={"gpu": N} does act locally: it sets CUDA_VISIBLE_DEVICES per worker |
Expecting a local memory cap leads to debugging the wrong layer. On Kubernetes the real limits come from the deploy config |
traffic={"timeout": 60} is the default |
Long generation gets cut off at 60 s with no other explanation |
Files: take a pathlib.Path in; write outputs into ctx.temp_dir and return the Path |
Anything else leaks temp files across requests |
Raise bentoml.exceptions.* subclasses for client-visible errors (InvalidArgument -> 400); codes 401, 403 and >= 500 are reserved |
Otherwise every failure is a 500 |
/, /livez, /readyz, /healthz, /metrics, /docs.json are taken by the server |
An API or a mounted app route with one of those paths is silently shadowed — mount ASGI apps under a prefix |
Multiple Services in one service.py: give each its own @bentoml.service, wire them with
dep = bentoml.depends(Other) at class scope, and call self.dep.method(...) like a local
method. The Service you serve is the entry service; BentoML starts its dependencies with
it. Split only for a real reason — different hardware, independent scaling, or a shared
downstream — since each split adds a network hop. Details: distributed services.
IO types beyond str/int/dict (numpy, pandas, PIL.Image, Path, pydantic models, root
input, validators, streaming, batching): io-types. Config keys: service-config. Task
queues, LLM streaming, model composition, FastAPI mounts, Gradio UIs: recipes.
Step 3 — declare the runtime environment
image = bentoml.images.Image(python_version="3.11").requirements_file("requirements.txt")
@bentoml.service(image=image)
class Summarization: ...
Image is the current API (BentoML >= 1.3.20) and lives next to the code it describes.
Chainable: .python_packages(...), .requirements_file(...), .system_packages(...),
.run("cmd"), .run_script("scripts/setup.sh"), .build_include(...). .run() is
position-sensitive: before .python_packages() runs before pip, after it runs after.
bentoml itself is added automatically. Don't list it.
- Versions are locked at build time unless you pass
lock_python_packages=False.
- A multi-Service bento uses only the entry Service's image; per-Service images are not
supported yet, so put every dependency there.
.bentoignore (gitignore syntax) is what keeps the bento small — always exclude the venv,
__pycache__/, datasets and checkpoints.
Step 4 — serve and verify (never skip)
bentoml serve # resolves bentofile.yaml -> pyproject.toml -> service.py
bentoml serve service:Summarization --reload -p 3000 # explicit target, dev reload
Then, in this order — each check catches a different class of mistake:
curl -sf localhost:3000/readyz # 1. it started and loaded the model
curl -s localhost:3000/docs.json | head -c 400 # 2. the schema is what you intended
curl -s -X POST localhost:3000/summarize \ # 3. the anchor case, judged by its BODY
-H 'Content-Type: application/json' -d '{"text": "..."}'
Judge the response body against the anchor from §0, not the status code — a bento that
returns null, an empty string or a stack trace with a 200 is a failed verification. For a
conversion, run the original code on the same input and diff the two outputs; report the
comparison, and if they differ, say so rather than adjusting the expectation.
Commit the anchor as a test. No server needed:
import pytest
from starlette.testclient import TestClient
from service import Summarization
@pytest.fixture(scope="session") # to_asgi() ONCE per process: it registers
def client(): # Prometheus collectors, and a second call
with TestClient(Summarization.to_asgi()) as c: # raises "Duplicated timeseries"
yield c
def test_summarize(client): # TestClient must be a context manager
r = client.post("/summarize", json={"text": "..."})
assert r.status_code == 200 and "expected substring" in r.text
Summarization() can also be instantiated directly and called as plain Python — the fastest
loop while iterating. More: testing.
Step 5 — build and hand off
bentoml build # -> summarization:xxxxxxxx (packages the cwd, minus .bentoignore)
bentoml list
bentoml serve summarization:latest # serve the built artifact, not the source
The bento name is the entry Service's name in snake_case (DiabetesRisk -> diabetes_risk) —
that, not the class name, is what the deploy skills and bentoml containerize take.
A built Bento is the input to bentoml-containerize (image + registry push), then
bentoml-k8s-deploy or bentoml-ec2-deploy. Tell the user that is the next step; do not
build images or touch a cluster from this skill.
When it goes wrong
| Symptom |
Cause |
Fix |
does not contain a valid bentofile.yaml or service.py |
Wrong cwd, or the file isn't named service.py |
cd to the project, or bentoml serve mymodule:MyService |
Model NotFound at deploy, works locally |
Model reference created inside __init__ |
Move it to class scope |
400 + a pydantic detail array on an obviously valid request |
Annotation mismatch; a missing = None on an Optional; a batchable API sent a scalar; a ContentType-annotated upload whose client-declared MIME type differs (curl -F f=@x.csv sends application/octet-stream) |
Read the detail array, then /docs.json — it shows exactly what the server expects |
| Import error at serve time only |
A heavy import under bentoml.importing() is genuinely missing from the runtime env |
Add it to requirements.txt / .python_packages() |
| Throughput ~1 request at a time |
Sync API, threads=1 |
async def, or threads=N, or workers=N |
| Request dies at exactly 60 s |
Default traffic.timeout |
Raise it on the Service |
/readyz flaps under load |
Blocking call inside an async def |
Move to a sync API or to_async/to_thread |
| Bento build takes forever, artifact is huge |
Datasets/venv/checkpoints packaged |
.bentoignore |
| Dependency Service is never called |
Depended-on class not @bentoml.service, or depends() result not a class attribute |
Both are required |
| GPU idle |
workers="cpu_count" skips GPU assignment |
Set an explicit workers=N with resources={"gpu": N} |
References
Local, load when the task needs them:
| File |
Contents |
| io-types |
Every supported input/output type, validators, root input, files/images/tensors/dataframes, streaming, batching, what is not supported |
| service-config |
@bentoml.service keys: workers, threads, traffic, resources, envs, labels, http, metrics, endpoints, cmd; which are local vs. platform-only |
| recipes |
Copy-pasteable: sklearn/XGBoost, HF transformers, LLM streaming, multi-Service composition, adaptive batching, task queue, FastAPI mount, Gradio UI, external server via cmd |
| conversion |
Per-source playbooks (script, notebook, FastAPI, Flask, MLflow, BentoML 1.1) and the parity procedure |
Upstream docs (authoritative, versioned): Services, IO types, runtime environment,
model loading, distributed services, batching, tasks, streaming, ASGI, hooks,
clients, testing, build options, SDK reference, examples.
1---2name: bentoml-create-bento3description: Create a BentoML project: the `service.py` whose typed `@bentoml.api` methods become an HTTP API, plus its runtime image and a built Bento. Writes one from scratch, or converts existing code — a script, a notebook, a FastAPI/Flask app, an MLflow model, a BentoML 1.1 Runner project. Use for "create a bento", "wrap my model in BentoML", "convert my FastAPI app to BentoML", "add an endpoint to my bento", or a bento that will not start, rejects valid input, or serves one request at a time.4license: Apache-2.05---67# Create a BentoML project89A **Bento** is the artifact `bentoml build` produces; a **project** is the directory it is10built from, normally one `service.py` plus its dependencies. `@bentoml.service` on a class11makes it a **Service** — the unit that gets deployed and scaled, and there may be several in12one bento — and `@bentoml.api` on a method makes it an HTTP endpoint whose **schema is the13method's type annotations**. BentoML supplies the server, the OpenAPI spec, the Swagger UI,14the client, health checks and metrics. You write the class.1516Two paths, same steps 2-5: **A** from scratch (§1A) · **B** convert existing code (§1B).1718It also covers an existing project: adding an endpoint, switching an API to streaming or19adaptive batching, moving long work to a background task, splitting one Service into several,20and diagnosing a bento that will not start, rejects valid input, or serves one request at a21time (last section).2223## What you produce2425| File | When | Purpose |26|---|---|---|27| `service.py` | always | The Services and their APIs. The whole surface. |28| `requirements.txt` | always | Runtime deps, referenced by the `Image` and pip-installed locally |29| `.bentoignore` | always | Keeps data, venvs, checkpoints, `__pycache__` out of the bento |30| `test_service.py` | always | The anchor case as a test (§4) |31| `save_model.py` | own weights | One-shot script that puts weights in the model store |32| `bentofile.yaml` / `pyproject.toml` | legacy only | Alternative to `Image`; use `Image` instead ([build options]) |3334No Dockerfile, no `uvicorn`/`gunicorn` call, no `main()`, no route table, no request/response35plumbing. If you are writing any of those, you are working against the framework.3637## Step 0 — get the anchor case3839Before writing anything, get **one concrete input and its expected output** from the user (or40from the existing code's tests/README). This is the anchor: §4 verifies against it. Without it41you can only confirm that a bento returns *something*, which is how a wrong one ships.4243If the user cannot supply one, derive it from the model card / existing code and state the44assumption explicitly.4546## Step 1A — new project: ask once4748Ask these together, with defaults, in one round. Accept "you decide" for any of them.4950| Ask | Default when the user has no preference |51|---|---|52| What does it do, and is it one endpoint or several? | One, named after the verb (`summarize`, `classify`) |53| Where do the weights come from: a Hugging Face ID, local files, an existing model-store tag, or no model at all? | HF ID if the user named a model; else no model |54| Input and output types, per endpoint | `str -> str`, or the model's natural pair (§ [io-types]) |55| GPU required? | No |56| Python version and pinned packages? | Your current Python; unpinned packages |57| Response profile: interactive, long-running, token-streaming, or fire-and-forget? | Interactive |5859**Do not ask** about workers, threads, replicas, ports, batching, registries or timeouts. The60defaults are correct until something is measured, and every one of them is a one-line change61later.6263Then scaffold the file set above and go to §2. Keep the first version to one Service and one64API even if the target is bigger — get it green, then grow.6566## Step 1B — convert existing code6768Read the existing code first. Find and write down: (1) the inference entry point, (2) where the69weights live, (3) the real dependency list, (4) the anchor case. Then translate:7071| In the existing code | In the Bento |72|---|---|73| Module-level model load, or a `load_model()` called per request | `__init__` — runs once per worker |74| A pickle / `.pt` / directory of weights | `save_model.py` once, then a class attribute `BentoModel("name:latest")` |75| `from_pretrained("org/model")` | class attribute `HuggingFaceModel("org/model")`, pass the returned path |76| `@app.post("/predict")` handler | `@bentoml.api def predict(...) -> ...` with annotations |77| Pydantic request/response models | Keep them verbatim — use as a parameter type, or `@bentoml.api(input_spec=Model)` |78| `argparse` / `if __name__ == "__main__"` | Delete. `bentoml serve` is the entry point |79| `uvicorn.run(...)`, gunicorn worker count | Delete; `@bentoml.service(workers=N)` |80| `Dockerfile`, `pip install` lines | `bentoml.images.Image(...)` (§3) |81| Custom routes, static files, webhooks, a UI | Keep the FastAPI app as-is and mount it under a prefix: `@bentoml.asgi_app(app, path="/v1")` |82| Auth / rate limiting as a route dependency | ASGI middleware (`Service.add_asgi_middleware`) — FastAPI `Depends` never sees `@bentoml.api` routes |83| A Flask/Django (WSGI) app | Port the inference route to `@bentoml.api`; a WSGI app needs an ASGI adapter to mount |84| BentoML 1.1 `Runner` / `.to_runner()` | A second `@bentoml.service` + `bentoml.depends()`; `bentoml.runner_service(runner)` is a stopgap |85| Celery / RQ / a background thread | `@bentoml.task` (§ [recipes]) |86| A notebook | Cells to `__init__` (setup) and one `@bentoml.api` (the inference cell) |8788Rules for a conversion: **do not rewrite the model code.** Import it, call it, keep it89importable so the old and new paths can be diffed. Convert the smallest slice that serves the90anchor case, verify parity (§4), then move the rest. Detail and per-source playbooks:91[conversion].9293## Step 2 — write `service.py`9495```python96from __future__ import annotations9798import bentoml99from bentoml.models import HuggingFaceModel100101with bentoml.importing(): # heavy/optional imports: skipped at build, required at serve102 from transformers import pipeline103104105@bentoml.service(106 resources={"cpu": "2"}, # deployment hint (see rules), not a local limit107 traffic={"timeout": 30}, # seconds; default 60108)109class Summarization:110 model_path = HuggingFaceModel("sshleifer/distilbart-cnn-12-6") # class scope, always111112 def __init__(self) -> None: # once per worker113 self.pipeline = pipeline("summarization", model=self.model_path)114115 @bentoml.api # -> POST /summarize116 def summarize(self, text: str) -> str:117 return self.pipeline(text)[0]["summary_text"]118```119120Rules that are not guessable from the API surface:121122| Rule | Why it bites |123|---|---|124| Model references (`HuggingFaceModel`, `BentoModel`) go at **class scope**, not in `__init__` | Class scope is what declares the model a dependency of the bento. Inside `__init__` it is not packaged, and deployment fails with model `NotFound` |125| Annotate **every** parameter and the return | The annotations *are* the schema, the OpenAPI spec and the client. An unannotated parameter becomes `Any`: no validation, no docs, no client typing |126| `Optional[X]` requires an explicit default (`= None`); other unions are unsupported | Silent schema surprise otherwise |127| Load models in `__init__`, never inside an API method | Per-request loading is the single most common cause of "BentoML is slow" |128| Service name = class name; `@bentoml.service(name="...")` overrides. API route = `/<method_name>`; `@bentoml.api(route=...)` overrides | Deploy configs, `BENTOML_SERVE_DEPENDS` and clients all key off these names |129| A method name may not start with `__` | Raises at import |130| A **sync** API runs in a thread pool of size `threads` (default **1**) — one request at a time per worker | Concurrency comes from `async def`, or `threads=N`, or `workers=N`. Not from adding replicas |131| Never block inside an `async def` API | It stalls the event loop; `/readyz` starts failing under load. Use `await self.dep.to_async.method(...)`, or `anyio.to_thread.run_sync` |132| `resources={"cpu": ..., "memory": ...}` is a **hint for the deployment platform** — `bentoml serve` does not enforce it. `resources={"gpu": N}` does act locally: it sets `CUDA_VISIBLE_DEVICES` per worker | Expecting a local memory cap leads to debugging the wrong layer. On Kubernetes the real limits come from the deploy config |133| `traffic={"timeout": 60}` is the default | Long generation gets cut off at 60 s with no other explanation |134| Files: take a `pathlib.Path` in; write outputs into `ctx.temp_dir` and return the `Path` | Anything else leaks temp files across requests |135| Raise `bentoml.exceptions.*` subclasses for client-visible errors (`InvalidArgument` -> 400); codes 401, 403 and >= 500 are reserved | Otherwise every failure is a 500 |136| `/`, `/livez`, `/readyz`, `/healthz`, `/metrics`, `/docs.json` are taken by the server | An API or a mounted app route with one of those paths is silently shadowed — mount ASGI apps under a prefix |137138Multiple Services in one `service.py`: give each its own `@bentoml.service`, wire them with139`dep = bentoml.depends(Other)` at class scope, and call `self.dep.method(...)` like a local140method. The Service you serve is the **entry** service; BentoML starts its dependencies with141it. Split only for a real reason — different hardware, independent scaling, or a shared142downstream — since each split adds a network hop. Details: [distributed services].143144IO types beyond `str`/`int`/`dict` (numpy, pandas, `PIL.Image`, `Path`, pydantic models, root145input, validators, streaming, batching): [io-types]. Config keys: [service-config]. Task146queues, LLM streaming, model composition, FastAPI mounts, Gradio UIs: [recipes].147148## Step 3 — declare the runtime environment149150```python151image = bentoml.images.Image(python_version="3.11").requirements_file("requirements.txt")152153@bentoml.service(image=image)154class Summarization: ...155```156157- `Image` is the current API (BentoML >= 1.3.20) and lives next to the code it describes.158 Chainable: `.python_packages(...)`, `.requirements_file(...)`, `.system_packages(...)`,159 `.run("cmd")`, `.run_script("scripts/setup.sh")`, `.build_include(...)`. `.run()` is160 position-sensitive: before `.python_packages()` runs before pip, after it runs after.161- `bentoml` itself is added automatically. Don't list it.162- Versions are locked at build time unless you pass `lock_python_packages=False`.163- A multi-Service bento uses **only the entry Service's image**; per-Service images are not164 supported yet, so put every dependency there.165- `.bentoignore` (gitignore syntax) is what keeps the bento small — always exclude the venv,166 `__pycache__/`, datasets and checkpoints.167168## Step 4 — serve and verify (never skip)169170```bash171bentoml serve # resolves bentofile.yaml -> pyproject.toml -> service.py172bentoml serve service:Summarization --reload -p 3000 # explicit target, dev reload173```174175Then, in this order — each check catches a different class of mistake:176177```bash178curl -sf localhost:3000/readyz # 1. it started and loaded the model179curl -s localhost:3000/docs.json | head -c 400 # 2. the schema is what you intended180curl -s -X POST localhost:3000/summarize \ # 3. the anchor case, judged by its BODY181 -H 'Content-Type: application/json' -d '{"text": "..."}'182```183184Judge the **response body** against the anchor from §0, not the status code — a bento that185returns `null`, an empty string or a stack trace with a 200 is a failed verification. For a186conversion, run the original code on the same input and diff the two outputs; report the187comparison, and if they differ, say so rather than adjusting the expectation.188189Commit the anchor as a test. No server needed:190191```python192import pytest193from starlette.testclient import TestClient194from service import Summarization195196@pytest.fixture(scope="session") # to_asgi() ONCE per process: it registers197def client(): # Prometheus collectors, and a second call198 with TestClient(Summarization.to_asgi()) as c: # raises "Duplicated timeseries"199 yield c200201def test_summarize(client): # TestClient must be a context manager202 r = client.post("/summarize", json={"text": "..."})203 assert r.status_code == 200 and "expected substring" in r.text204```205206`Summarization()` can also be instantiated directly and called as plain Python — the fastest207loop while iterating. More: [testing].208209## Step 5 — build and hand off210211```bash212bentoml build # -> summarization:xxxxxxxx (packages the cwd, minus .bentoignore)213bentoml list214bentoml serve summarization:latest # serve the built artifact, not the source215```216217The bento name is the entry Service's name in snake_case (`DiabetesRisk` -> `diabetes_risk`) —218that, not the class name, is what the deploy skills and `bentoml containerize` take.219220A built Bento is the input to **`bentoml-containerize`** (image + registry push), then221`bentoml-k8s-deploy` or `bentoml-ec2-deploy`. Tell the user that is the next step; do not222build images or touch a cluster from this skill.223224## When it goes wrong225226| Symptom | Cause | Fix |227|---|---|---|228| `does not contain a valid bentofile.yaml or service.py` | Wrong cwd, or the file isn't named `service.py` | `cd` to the project, or `bentoml serve mymodule:MyService` |229| Model `NotFound` at deploy, works locally | Model reference created inside `__init__` | Move it to class scope |230| `400` + a pydantic `detail` array on an obviously valid request | Annotation mismatch; a missing `= None` on an `Optional`; a batchable API sent a scalar; a `ContentType`-annotated upload whose client-declared MIME type differs (`curl -F f=@x.csv` sends `application/octet-stream`) | Read the `detail` array, then `/docs.json` — it shows exactly what the server expects |231| Import error at serve time only | A heavy import under `bentoml.importing()` is genuinely missing from the runtime env | Add it to `requirements.txt` / `.python_packages()` |232| Throughput ~1 request at a time | Sync API, `threads=1` | `async def`, or `threads=N`, or `workers=N` |233| Request dies at exactly 60 s | Default `traffic.timeout` | Raise it on the Service |234| `/readyz` flaps under load | Blocking call inside an `async def` | Move to a sync API or `to_async`/`to_thread` |235| Bento build takes forever, artifact is huge | Datasets/venv/checkpoints packaged | `.bentoignore` |236| Dependency Service is never called | Depended-on class not `@bentoml.service`, or `depends()` result not a class attribute | Both are required |237| GPU idle | `workers="cpu_count"` skips GPU assignment | Set an explicit `workers=N` with `resources={"gpu": N}` |238239## References240241Local, load when the task needs them:242243| File | Contents |244|---|---|245| [io-types] | Every supported input/output type, validators, root input, files/images/tensors/dataframes, streaming, batching, what is *not* supported |246| [service-config] | `@bentoml.service` keys: workers, threads, traffic, resources, envs, labels, http, metrics, endpoints, `cmd`; which are local vs. platform-only |247| [recipes] | Copy-pasteable: sklearn/XGBoost, HF transformers, LLM streaming, multi-Service composition, adaptive batching, task queue, FastAPI mount, Gradio UI, external server via `cmd` |248| [conversion] | Per-source playbooks (script, notebook, FastAPI, Flask, MLflow, BentoML 1.1) and the parity procedure |249250Upstream docs (authoritative, versioned): [Services], [IO types], [runtime environment],251[model loading], [distributed services], [batching], [tasks], [streaming], [ASGI], [hooks],252[clients], [testing], [build options], [SDK reference], [examples].253254[io-types]: references/io-types.md255[service-config]: references/service-config.md256[recipes]: references/recipes.md257[conversion]: references/conversion.md258[Services]: https://docs.bentoml.com/en/latest/build-with-bentoml/services.html259[IO types]: https://docs.bentoml.com/en/latest/build-with-bentoml/iotypes.html260[runtime environment]: https://docs.bentoml.com/en/latest/build-with-bentoml/runtime-environment.html261[model loading]: https://docs.bentoml.com/en/latest/build-with-bentoml/model-loading-and-management.html262[distributed services]: https://docs.bentoml.com/en/latest/build-with-bentoml/distributed-services.html263[batching]: https://docs.bentoml.com/en/latest/get-started/adaptive-batching.html264[tasks]: https://docs.bentoml.com/en/latest/get-started/async-task-queues.html265[streaming]: https://docs.bentoml.com/en/latest/build-with-bentoml/streaming.html266[ASGI]: https://docs.bentoml.com/en/latest/build-with-bentoml/asgi.html267[hooks]: https://docs.bentoml.com/en/latest/build-with-bentoml/lifecycle-hooks.html268[clients]: https://docs.bentoml.com/en/latest/build-with-bentoml/clients.html269[testing]: https://docs.bentoml.com/en/latest/build-with-bentoml/testing.html270[build options]: https://docs.bentoml.com/en/latest/reference/bentoml/bento-build-options.html271[SDK reference]: https://docs.bentoml.com/en/latest/reference/bentoml/sdk.html272[examples]: https://docs.bentoml.com/en/latest/examples/overview.html