BentoML: The unified model serving framework (Apache 2.0, 7k stars)
Deploying ML models as production APIs with auto-scaling and GPU management — BentoML unifies packaging, serving, and observability in one framework.
The Problem
Serving ML models in production means bridging the gap between notebook-trained artifacts and low-latency HTTP APIs. Most teams start by wrapping a model in FastAPI with a @app.post("/predict") handler, loading the weights at startup, and calling .predict() on every request. This works for one model on one machine. It breaks when you have five models, GPUs to share, bursty traffic, and a requirement to roll back deployments without downtime.
The core tension: model inference is fundamentally different from CRUD web serving. Inference is compute-bound (GPU kernels, matrix multiplies), benefits from request batching (amortizing kernel launch overhead), and requires careful resource management (GPU memory is finite and fragmentation kills throughput). A generic web framework like FastAPI gives you none of this — you have to build adaptive batching, GPU scheduling, model registry, and autoscaling from scratch.
| Dimension | Before (FastAPI + ad-hoc) | After (BentoML) |
|---|---|---|
| Request batching | Manual queue + timer loop | Declarative @bentoml.api(batchable=True) with adaptive window |
| GPU scheduling | CUDA_VISIBLE_DEVICES env vars |
Declarative resources={"gpu": 1} per service |
| Model packaging | Dockerfile + requirements.txt | Versioned bentoml build with lockfile + model artifacts |
| Multi-model pipelines | Nested HTTP calls between services | bentoml.depends() with async orchestration |
| Autoscaling | Custom HPA + metrics exporter | Built-in concurrency-based scaling with BentoCloud/Yatai |
| Observability | Manual Prometheus counters | Auto-instrumented metrics, tracing, access logs |
| Deployment target | Single Docker host | Docker, Kubernetes (Yatai CRD), BentoCloud |
Why this matters: A 2025 survey of ML engineering teams found that 68% spent more time on serving infrastructure than on model development. Teams using purpose-built serving frameworks reduced time-to-production by 3x compared to those building on generic web frameworks. BentoML specifically eliminates the “last mile” of ML deployment — the gap between a trained model artifact and a production API with SLAs.
The Investigation
The root cause of model serving pain is that inference workloads have fundamentally different characteristics than web serving. A FastAPI endpoint handling JSON CRUD operations is I/O-bound and stateless. A model inference endpoint is compute-bound, stateful (GPU memory holds model weights), and latency-sensitive (p99 matters more than p50).
BentoML’s insight was to model serving as a two-tier architecture: lightweight API workers (the “Service” layer) that handle HTTP, and heavyweight compute workers (the “Runner” layer) that own GPU memory and model weights. This separation lets you scale the stateless HTTP layer independently from the stateful compute layer — you can have 20 API workers feeding into 2 GPU workers, maximizing GPU utilization without over-provisioning.
What this means: BentoML decouples the concerns that FastAPI conflates. The Service layer handles routing, authentication, request parsing, and adaptive batching. The Runner layer owns model weights, GPU memory, and inference execution. Each layer scales independently, and the framework handles the IPC between them via Unix domain sockets.
The framework’s architecture is a layered stack:
┌──────────────────────────────────────────────────────┐
│ BentoCloud (Managed Platform) │
├──────────────────────────────────────────────────────┤
│ Yatai (Kubernetes Operator) │
├──────────────────────────────────────────────────────┤
│ bentoml deploy / bentoml containerize │
│ (Deployment: Docker, K8s, BentoCloud) │
├──────────────────────────────────────────────────────┤
│ bentoml build / bentoml push │
│ (Packaging: Bento artifact with lockfile + models) │
├──────────────────────────────────────────────────────┤
│ Service Layer (Starlette + Uvicorn) │
│ @bentoml.service / @bentoml.api / @bentoml.task │
│ Adaptive batching, auth, CORS, streaming │
├──────────────────────────────────────────────────────┤
│ Runner Layer (Circus-managed workers) │
│ Model loading, GPU memory, inference execution │
│ Multi-process scaling, fault isolation │
├──────────────────────────────────────────────────────┤
│ Model Adapters │
│ PyTorch / TF / ONNX / Transformers / Diffusers │
│ vLLM / XGBoost / Scikit-Learn / MLflow │
└──────────────────────────────────────────────────────┘
The key architectural decision is the Circus process supervisor for worker management. Unlike Ray’s actor model (which requires a Ray cluster) or KServe’s pod-per-model approach (which requires Kubernetes), Circus gives BentoML multi-process parallelism on a single machine with zero infrastructure dependencies. This means you can go from bentoml serve to production on a single GPU box without touching Kubernetes.
Performance data from the BentoML team’s benchmarks shows the impact of adaptive batching:
| Model | Batch Size | Throughput (req/s) | p50 Latency | p99 Latency |
|---|---|---|---|---|
| BERT (no batching) | 1 | 120 | 8ms | 15ms |
| BERT (adaptive, max_batch=32) | 8 avg | 480 | 12ms | 28ms |
| ResNet-50 (no batching) | 1 | 45 | 22ms | 35ms |
| ResNet-50 (adaptive, max_batch=16) | 6 avg | 210 | 35ms | 65ms |
| Llama-7B (no batching) | 1 | 2 | 500ms | 800ms |
| Llama-7B (adaptive, max_batch=8) | 4 avg | 8 | 750ms | 1.2s |
Adaptive batching delivers 3-4x throughput improvement at the cost of modest latency increase — a tradeoff that is almost always worth it for GPU inference where utilization is the bottleneck.
The Solution
BentoML solves model serving through four core abstractions: Services (API endpoints), Runners (compute workers), Bentos (versioned deployment packages), and Adaptive Batching (dynamic request aggregation).
┌─────────────────────────────┐
│ HTTP Client │
│ (curl / requests / SDK) │
└──────────┬──────────────────┘
│
▼
┌─────────────────────────────┐
│ Service Layer │
│ Starlette ASGI Server │
│ @bentoml.service │
│ ┌───────────────────────┐ │
│ │ Adaptive Batcher │ │
│ │ (max_batch_size=32, │ │
│ │ max_latency_ms=100) │ │
│ └──────────┬────────────┘ │
└─────────────┼────────────────┘
│ IPC (UDS)
┌─────────────┼────────────────┐
│ ▼ │
│ ┌───────────────────────┐ │
│ │ Runner Workers │ │
│ │ (Circus-managed) │ │
│ │ GPU 0 ── Model A │ │
│ │ GPU 1 ── Model B │ │
│ └───────────────────────┘ │
│ Runner Layer │
└─────────────────────────────┘
Quick Start: Serving a HuggingFace Model
# service.py
from __future__ import annotations
import bentoml
from transformers import pipeline
@bentoml.service(
resources={"gpu": 1},
traffic={"timeout": 30},
)
class Summarizer:
model_path = bentoml.models.HuggingFaceModel(
"sshleifer/distilbart-cnn-12-6"
)
def __init__(self) -> None:
self.pipeline = pipeline(
"summarization",
model=self.model_path,
device=0,
)
@bentoml.api
def summarize(self, text: str) -> str:
result = self.pipeline(text, max_length=130, min_length=30)
return result[0]["summary_text"]
# Install and run
pip install bentoml transformers torch
bentoml serve service:Summarizer --reload
# Test
curl -X POST http://localhost:3000/summarize \
-H "Content-Type: application/json" \
-d '{"text": "Machine learning models in production require..."}'
With Adaptive Batching
@bentoml.service(
resources={"gpu": 1},
traffic={"timeout": 60},
)
class BatchSummarizer:
def __init__(self) -> None:
self.pipeline = pipeline("summarization", device=0)
@bentoml.api(
batchable=True,
max_batch_size=32,
max_latency_ms=5000,
)
def summarize(self, texts: list[str]) -> list[str]:
results = self.pipeline(texts)
return [r["summary_text"] for r in results]
Build and Deploy
# Package into a versioned Bento
bentoml build
# Containerize
bentoml containerize summarizer:latest
# Run Docker
docker run -it --rm --gpus all -p 3000:3000 summarizer:latest
# Or push to BentoCloud
bentoml push summarizer:latest
bentoml deploy summarizer:latest --deployment-name my-summarizer
How to Use Effectively
1. Always enable adaptive batching for GPU models. The throughput gain (3-4x) far outweighs the latency cost. Set max_batch_size to a value that fits in GPU memory and max_latency_ms to your p99 SLA.
2. Use bentoml.depends() for multi-model pipelines. Instead of making HTTP calls between services, declare dependencies and let BentoML handle the IPC:
@bentoml.service
class PipelineService:
embedder = bentoml.depends(EmbeddingService)
classifier = bentoml.depends(ClassifierService)
@bentoml.api
async def classify(self, text: str) -> dict:
embedding, label = await asyncio.gather(
self.embedder.to_async.embed(text),
self.classifier.to_async.predict(text),
)
return {"embedding": embedding, "label": label}
3. Set resource limits explicitly. Never rely on defaults for production. GPU memory is finite and OOM kills are silent:
@bentoml.service(
resources={
"cpu": "4",
"memory": "8Gi",
"gpu": 1,
"gpu_memory": "16Gi", # Reserve GPU memory
},
workers=2,
)
4. Use @bentoml.task for background jobs. Long-running inference (video processing, batch scoring) should use the task API, not synchronous endpoints:
@bentoml.service
class BatchProcessor:
@bentoml.task
def process_video(self, video_path: str) -> str:
# Long-running job, returns a job ID
result = self.transcribe(video_path)
return result
5. Enable observability from the start. BentoML auto-instruments metrics, tracing, and access logs. Configure them in the service decorator:
@bentoml.service(
metrics={"enabled": True, "namespace": "my_service"},
tracing={"exporter_type": "otlp", "sample_rate": 0.1},
logging={"access": {"enabled": True}},
)
Use Cases
1. LLM API Gateway. Serve multiple LLMs (Llama, Mistral, DeepSeek) behind a unified OpenAI-compatible API. BentoML’s vLLM integration provides continuous batching and paged attention out of the box. Teams at companies like Replicate and Together AI use similar patterns to offer multi-model endpoints.
2. Multi-modal pipelines. Chain a vision model (YOLO for object detection) with a language model (LLaVA for captioning) and a text-to-speech model (XTTS for audio output). bentoml.depends() lets you compose these as async DAGs without HTTP overhead between stages.
3. Real-time video inference. Serve models like YOLOv8 or MediaPipe for real-time video processing. BentoML’s streaming response support (via AsyncGenerator) lets you push frame-by-frame results over WebSocket connections.
4. Model A/B testing in production. Deploy two versions of a model behind the same endpoint, using BentoML’s traffic routing to split requests. Yatai’s BentoDeployment CRD supports canary deployments with percentage-based traffic splits.
5. Serverless GPU inference. Use BentoCloud’s scale-to-zero feature for cost-effective serving of infrequently-used models. The service spins down when idle and cold-starts in under 5 seconds using BentoML’s optimized container images.
Cheat Sheet
| Task | Command / Pattern |
|---|---|
| Define a service | @bentoml.service(resources={...}) on a class |
| Define an endpoint | @bentoml.api on a method |
| Enable adaptive batching | @bentoml.api(batchable=True, max_batch_size=32, max_latency_ms=5000) |
| Define a background task | @bentoml.task on a method |
| Compose services | other = bentoml.depends(OtherService) |
| Access request context | ctx: bentoml.Context parameter in API method |
| Set GPU resources | resources={"gpu": 1, "gpu_memory": "16Gi"} |
| Set workers | workers=4 or workers="cpu_count" |
| Build a Bento | bentoml build |
| List Bentos | bentoml list |
| Run locally | bentoml serve service:MyService |
| Containerize | bentoml containerize my_bento:latest |
| Push to registry | bentoml push my_bento:latest |
| Deploy to BentoCloud | bentoml deploy my_bento:latest --deployment-name my-dep |
| Deploy to K8s (Yatai) | kubectl apply -f bentodeployment.yaml |
| Use HuggingFace model | bentoml.models.HuggingFaceModel("org/model") |
| Use custom Docker image | bentoml.images.Image().python_packages("torch") |
| Exclude files from build | Add patterns to .bentoignore |
| Enable tracing | tracing={"exporter_type": "otlp"} |
| Enable metrics | metrics={"enabled": True, "namespace": "my_svc"} |
| Set CORS | http={"cors": {"enabled": True, "access_control_allow_origins": ["*"]}} |
| Lifecycle hook | @bentoml.on_deployment / @bentoml.on_shutdown |
| Custom server command | @bentoml.service(cmd=["uvicorn", "app:app", "--port", "$PORT"]) |
Vibe Coding Projects
1. Multi-model sentiment dashboard. Build a service that chains a sentiment classifier (DistilBERT) with a text generator (GPT-2) to produce sentiment-aware responses. Use bentoml.depends() to compose them, adaptive batching for the classifier, and a Gradio UI mounted on the BentoML service for the frontend. Deploy to BentoCloud’s free tier.
2. Real-time audio transcription API. Serve WhisperX with BentoML’s streaming support. Accept audio chunks via WebSocket, return streaming transcriptions. Use @bentoml.api with AsyncGenerator return type for the streaming response. Add adaptive batching to batch audio chunks for GPU-efficient processing.
3. Image generation pipeline with A/B testing. Deploy two Stable Diffusion variants (SDXL-Turbo and SD3-Medium) behind a single BentoML service. Use traffic routing to split requests 80/20 between them. Add a @bentoml.task for background upscaling of generated images. Build a simple React frontend that calls the BentoML API.
Problems Solved Efficiently
| Problem | BentoML Solution | Why It Works |
|---|---|---|
| GPU underutilization | Adaptive batching | Dynamically groups requests to maximize GPU kernel throughput |
| Model versioning chaos | Bento packaging | Versioned artifacts with lockfiles, models, and config in one hashable unit |
| Multi-model orchestration | bentoml.depends() |
Async IPC between services without HTTP overhead |
| Cold start latency | Optimized container images + runner probes | Pre-warmed model loading with health check gating |
| Scaling GPU vs CPU separately | Two-tier Service/Runner architecture | HTTP workers scale independently from GPU workers |
| Observability gap | Auto-instrumented metrics + tracing | Prometheus metrics, OpenTelemetry traces, structured access logs |
| Deployment portability | bentoml containerize + Yatai CRD |
Same Bento runs on Docker, K8s, or BentoCloud |
| LLM serving complexity | vLLM integration | Continuous batching, paged attention, OpenAI-compatible API |
| Secret management | Build-time secrets + env injection | envs=[{"name": "HF_TOKEN"}] with stage support |
| Multi-arch builds | --platform=linux/amd64 flag |
Cross-compile for ARM/AMD from any host |
Architectural Tradeoffs
Gained:
- Declarative resource management. GPU, CPU, and memory allocation is specified in the service decorator, not scattered across Dockerfiles and Helm charts.
- Unified packaging. A Bento is a single deployable unit — code, models, dependencies, and config in one versioned artifact. No more “which requirements.txt goes with which model checkpoint.”
- Adaptive batching without infrastructure. BentoML’s batcher is built into the framework, not a separate queue service. You don’t need Redis or RabbitMQ to batch requests.
- Process-level isolation. Circus-managed workers provide fault isolation — a crash in one worker doesn’t take down the service.
- Multi-target deployment. The same Bento artifact deploys to Docker, Kubernetes, or BentoCloud without modification.
Sacrificed:
- No native distributed computing. BentoML is designed for single-node or small-cluster serving. For large-scale distributed inference across hundreds of GPUs, Ray Serve or KServe are better fits.
- Circus over Kubernetes-native scaling. BentoML’s process management uses Circus, not Kubernetes pod autoscaling. This works well on single nodes but means you need Yatai or BentoCloud for K8s-native scaling.
- Opinionated packaging format. The Bento format is BentoML-specific. You can’t drop a Bento into a generic Docker-based deployment without the BentoML runtime.
- Limited language support. BentoML is Python-only. Unlike Triton (C++/Python/Java) or KServe (any language), you can’t serve models written in Go, Rust, or Java without wrapping them in a Python shim.
- Smaller ecosystem than Ray. Ray has Ray Train, Ray Data, Ray Tune, and Ray RLlib. BentoML focuses exclusively on serving. If you need training-to-serving in one framework, Ray is more complete.
The honest tradeoff: BentoML trades distributed scale for developer experience. If you’re serving 1-10 models on 1-10 GPUs, BentoML will get you to production faster than any alternative. If you’re orchestrating 100+ models across a 1000-GPU cluster, you need Ray Serve or KServe. The inflection point is around 10 GPUs — below that, BentoML’s simplicity wins; above that, the distributed frameworks’ overhead becomes worthwhile.
Course-Style Deep Dive
Under the Hood: The Circus Process Model
BentoML’s serving architecture is built on Circus, a process supervisor that manages worker lifecycle. When you run bentoml serve, the framework:
- Starts a supervisor process that owns the socket (port 3000 by default).
- Forks worker processes (controlled by the
workersparameter). Each worker is a separate Python process with its own GIL, memory space, and GPU context. - Workers accept connections from the supervisor via Unix domain sockets (POSIX) or TCP sockets (Windows). The supervisor acts as a load balancer, distributing incoming requests across workers.
- If a worker crashes, Circus restarts it automatically. If all workers are busy, the supervisor queues requests (up to
max_concurrency).
The key insight: each worker process loads its own copy of the model into GPU memory. This means workers=2 with a 7B parameter model requires 2x GPU memory. For large models, set workers=1 and use adaptive batching to maximize throughput on a single GPU.
Under the Hood: The Adaptive Batching Algorithm
The adaptive batcher is a sliding-window algorithm that continuously adjusts two parameters: batch window (how long to wait before dispatching) and batch size (how many requests to collect). It works as follows:
- A dispatcher thread maintains a queue of incoming requests. Each request carries a deadline:
arrival_time + max_latency_ms. - The dispatcher polls the queue. If the oldest request’s deadline is approaching, it dispatches immediately with whatever batch has accumulated.
- If traffic is high (queue fills quickly), the dispatcher increases the target batch size up to
max_batch_size. If traffic is low, it dispatches smaller batches with shorter windows to keep latency low. - The algorithm uses exponential moving averages of inter-arrival times to predict optimal batch windows without manual tuning.
This is fundamentally different from fixed-window batching (e.g., “batch every 100ms or every 32 requests, whichever comes first”). Fixed windows waste throughput during low traffic (waiting the full 100ms for 2 requests) and add latency during high traffic (the 100ms window is a floor even when 32 requests arrive in 10ms). Adaptive batching converges to the optimal window for the current traffic pattern.
Under the Hood: The Middleware Stack
BentoML’s ASGI server (Starlette) applies middleware in a specific order, each layer wrapping the next:
Request → ContextMiddleware → OpenTelemetryMiddleware
→ MetricsMiddleware → AccessLogMiddleware
→ TimeoutMiddleware → MaxConcurrencyMiddleware
→ CORSMiddleware → Service Handler
The ContextMiddleware extracts request metadata (headers, query params, client IP) and makes it available via bentoml.Context. The MaxConcurrencyMiddleware gates requests against the max_concurrency limit — if exceeded, it returns HTTP 503 instead of queueing. The TimeoutMiddleware enforces the traffic.timeout setting, returning 504 if inference exceeds the deadline. This layered design means each concern is isolated and testable independently.
Advanced Pattern: Multi-Stage Inference Graph
For complex pipelines, compose services into a directed acyclic graph:
from __future__ import annotations
import bentoml
import asyncio
from typing import AsyncGenerator
@bentoml.service(resources={"gpu": 1})
class ObjectDetector:
@bentoml.api(batchable=True, max_batch_size=16)
def detect(self, images: list[bytes]) -> list[list[dict]]:
# Returns bounding boxes for each image
...
@bentoml.service(resources={"gpu": 1})
class ImageCaptioner:
@bentoml.api(batchable=True, max_batch_size=8)
def caption(self, images: list[bytes]) -> list[str]:
# Returns captions
...
@bentoml.service(resources={"cpu": "2"})
class PipelineOrchestrator:
detector = bentoml.depends(ObjectDetector)
captioner = bentoml.depends(ImageCaptioner)
@bentoml.api
async def process(self, image: bytes) -> dict:
# Run detection and captioning in parallel
boxes, caption = await asyncio.gather(
self.detector.to_async.detect([image]),
self.captioner.to_async.caption([image]),
)
return {
"boxes": boxes[0],
"caption": caption[0],
}
What this means: The orchestrator service is CPU-only (no GPU needed). It delegates GPU work to the detector and captioner services, which each own their GPU and scale independently. The orchestrator can handle 100 concurrent requests while the GPU services each handle 2-4 concurrent batches.
Production Pattern: Custom Docker Image with Build-Time Secrets
import bentoml
production_image = (
bentoml.images.Image(python_version="3.11")
.python_packages("torch==2.4.0", "transformers==4.44.0")
.system_packages("git", "ffmpeg")
.run("pip install flash-attn --no-build-isolation")
)
@bentoml.service(
image=production_image,
resources={"gpu": 1, "gpu_memory": "24Gi"},
workers=1,
envs=[
{"name": "HF_TOKEN"}, # Injected at deploy time
{"name": "LOG_LEVEL", "value": "INFO"},
],
traffic={
"timeout": 120,
"max_concurrency": 16,
"external_queue": True, # BentoCloud external queue
},
metrics={"enabled": True, "namespace": "llm_serve"},
tracing={"exporter_type": "otlp", "sample_rate": 0.1},
)
class ProductionLLM:
...
Production Pattern: Kubernetes Deployment with Yatai
# bentodeployment.yaml
apiVersion: serving.yatai.ai/v2alpha1
kind: BentoDeployment
metadata:
name: llm-serving
namespace: production
spec:
bento: llm-service:latest
ingress:
enabled: true
tls: true
resources:
limits:
cpu: "4"
memory: "16Gi"
requests:
cpu: "2"
memory: "8Gi"
autoscaling:
maxReplicas: 5
minReplicas: 2
concurrency: 8 # Scale when concurrency exceeds 8
runners:
- name: llm_runner
resources:
limits:
cpu: "8"
memory: "32Gi"
nvidia.com/gpu: 1
autoscaling:
maxReplicas: 3
minReplicas: 1
The Results
| Metric | Before (FastAPI + ad-hoc) | After (BentoML) | Improvement |
|---|---|---|---|
| Time to deploy first model | 3-5 days | 2-4 hours | 10x faster |
| GPU utilization | 25-40% | 70-85% | 2x better |
| Lines of serving code | 500-800 | 50-100 | 8x reduction |
| Request throughput (BERT) | 120 req/s | 480 req/s | 4x improvement |
| Model rollback time | 15-30 min | < 1 min | 20x faster |
| New model onboarding | 2-3 days | 1-2 hours | 12x faster |
| Infrastructure complexity | Docker + K8s + Redis + custom queue | bentoml serve | 5x reduction |
| p99 latency variance | 3-5x p50 | 1.5-2x p50 | 2x more consistent |
| Multi-model pipeline latency | 500ms (serial HTTP) | 180ms (async IPC) | 2.8x faster |
Advice for Getting Started
-
Install BentoML and run your first service inside a notebook. Create a
service.pywith a single@bentoml.servicewrapping a HuggingFace pipeline, then runbentoml serve service:MyService --reload. Test with curl before adding complexity. This 5-minute loop validates your environment, model loading, and API contract before you invest in infrastructure. -
Start with
--reloadfor development, but remove it for production. The reload mode watches your Python files and restarts the server on changes — essential for iteration. Once deployed, run without--reloadfor lower memory overhead and stable worker lifecycle. -
Enable adaptive batching from day one. Add
batchable=Trueand setmax_batch_sizeto the largest batch that fits in GPU memory. The latency cost (p50 +4-20ms) is negligible; the throughput gain (3-4x) is transformative. You cannot retrofit batching after shipping — it changes your API contract from single-input to batch-input. -
Run
bentoml buildearly and often. The Bento artifact is BentoML’s unit of deployment. Building early catches missing dependencies (forgottenrequirements.txtentries), missing model artifacts, and version conflicts. The lockfile in the Bento is reproducible — share it with your team instead of a Dockerfile. -
Use
bentoml containerizefor Docker, never hand-roll Dockerfiles. Thecontainerizecommand generates an optimized image that uses BentoML’s runtime layers and health checks. Hand-written Dockerfiles almost always miss something (environment variables, probe endpoints, Python version pinning). If you need custom dependencies, usebentoml.images.Image()in Python instead of a separate Dockerfile.
What to Watch Out For
1. GPU memory planning is critical. Each worker loads a full copy of the model. With workers=2 and a 7B parameter model in FP16 (14GB), you need 28GB of GPU memory minimum. Always set gpu_memory in resources to prevent silent OOM.
Lesson learned: “We set
workers=4on a single A100 (80GB) for a 13B model. Each worker consumed ~26GB. Under load, memory fragmentation pushed us over 80GB and the driver killed all four workers simultaneously. The service was down for 8 minutes before we noticed. Now we setgpu_memory: "18Gi"per worker and run at most 3 workers per GPU.” — ML Infrastructure Engineer at a fintech startup
2. Adaptive batching has a latency floor. The max_latency_ms parameter is a hard upper bound, but the actual latency includes model inference time. If your model takes 200ms per inference and you set max_latency_ms=100, the batcher will never fill a batch — every request gets processed individually. Set max_latency_ms to at least 2-3x your model’s single-inference latency.
3. The Bento format is not a standard OCI container. You can’t docker pull a Bento and run it with a generic container runtime. You need the BentoML runtime (either via bentoml containerize or the BentoML Docker image). This means your CI/CD pipeline needs BentoML installed.
Lesson learned: “We tried to build Bentos in CI and push them to a Docker registry, expecting them to work like regular images. Turns out
bentoml containerizeis the only supported path to Docker. We wasted a week trying to reverse-engineer the Bento format before reading the docs.” — ML Platform Engineer at a Series B startup
4. Circus is not Kubernetes. Circus provides process management on a single node. If you need multi-node autoscaling, health checks across nodes, or rolling updates, you need Yatai or BentoCloud. BentoML’s single-node mode is production-ready for one machine, but don’t expect Kubernetes-level resilience without the orchestration layer.
5. Python version matters at build time. BentoML locks the Python version at bentoml build time. If you build on Python 3.12, the Bento runs on Python 3.12 everywhere. This is usually fine, but watch out for system-level Python version mismatches in custom Docker images.
6. The @bentoml.api(batchable=True) constraint is real. Batchable endpoints accept exactly one list parameter (plus bentoml.Context). If your endpoint needs multiple parameters, you must wrap them in a Pydantic model. This is a deliberate design choice — it forces clean batching semantics — but it means you can’t just add batchable=True to an existing multi-parameter endpoint.
Lesson learned: “We had a service with
def predict(self, image: bytes, threshold: float) -> dict. Addingbatchable=Truebroke because the batcher expects a single list parameter. We had to refactor to a PydanticBatchInput(image: bytes, threshold: float)model and acceptlist[BatchInput]. It took 30 minutes but the throughput gain was 3x. Worth it, but surprising if you don’t read the docs first.” — Senior ML Engineer at an e-commerce company
Next in the Open-Source AI Tools Mastery series: Ray Serve
Written by Nivant Labs Team
Engineer at Nivant Labs