import modal
# 1. Define minimal container image with pre-baked CUDA and PyTorch
app = modal.App("llm-inference-service")
cuda_image = (
modal.Image.debian_slim(python_version="3.11")
.pip_install("torch==2.3.0", "vllm==0.5.0", "transformers", "accelerate")
)
# 2. Attach high-speed Network Volume for model weight caching
model_volume = modal.Volume.from_name("model-cache-vol", create_if_missing=True)
@app.cls(
image=cuda_image,
gpu="A10G", # Specify accelerator tier
container_idle_timeout=120, # 2 min keep-alive prevents cold starts between requests
timeout=600,
volumes={"/root/.cache/huggingface": model_volume},
)
class LLMModelRunner:
@modal.enter()
def load_model(self):
"""Executes once during container boot or memory snapshot restoration."""
from vllm import LLM
print("Restoring model weights into GPU VRAM...")
self.llm = LLM(
model="mistralai/Mistral-7B-Instruct-v0.3",
gpu_memory_utilization=0.90,
max_model_len=4096,
)
@modal.method()
def generate(self, prompt: str) -> str:
"""Billed strictly per active millisecond while executing."""
from vllm import SamplingParams
sampling_params = SamplingParams(temperature=0.7, max_tokens=512)
outputs = self.llm.generate([prompt], sampling_params)
return outputs[0].outputs[0].text
@app.function()
@modal.web_endpoint(method="POST")
def api_handler(item: dict):
runner = LLMModelRunner()
result = runner.generate.remote(item.get("prompt", ""))
return {"response": result}
version: '3.8'
services:
# Dedicated vLLM GPU Container on AWS EC2 g5.xlarge (A10G 24GB)
# Flat cost: $1.006/hr on-demand ($724/mo) regardless of utilization
vllm-engine:
image: vllm/vllm-openai:latest
container_name: dedicated_gpu_inference
restart: always
runtime: nvidia
ports:
- "8000:8000"
environment:
- HUGGING_FACE_HUB_TOKEN=${HF_TOKEN}
- MODEL_NAME=mistralai/Mistral-7B-Instruct-v0.3
volumes:
- /mnt/nvme/hf_cache:/root/.cache/huggingface
command: >
--model mistralai/Mistral-7B-Instruct-v0.3
--tensor-parallel-size 1
--gpu-memory-utilization 0.92
--max-model-len 4096
--port 8000
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 15s
timeout: 5s
retries: 3
#!/usr/bin/env bash
# GPU Idle Watchdog: Polls nvidia-smi GPU utilization every 60 seconds
# If GPU load remains under 2% for 20 consecutive minutes, terminates/stops EC2 instance.
IDLE_LIMIT_MINUTES=20
IDLE_COUNTER=0
echo "Starting GPU Idle Watchdog..."
while true; do
# Extract current GPU compute utilization %
GPU_UTIL=$(nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits | head -n 1)
if [ "$GPU_UTIL" -lt 2 ]; then
IDLE_COUNTER=$((IDLE_COUNTER + 1))
echo "[$(date '+%Y-%m-%d %H:%M:%S')] GPU Idle: ${GPU_UTIL}% for ${IDLE_COUNTER}/${IDLE_LIMIT_MINUTES} mins"
else
IDLE_COUNTER=0
echo "[$(date '+%Y-%m-%d %H:%M:%S')] GPU Active: ${GPU_UTIL}%. Counter reset."
fi
if [ "$IDLE_COUNTER" -ge "$IDLE_LIMIT_MINUTES" ]; then
echo "GPU idle threshold reached (${IDLE_LIMIT_MINUTES}m). Halting instance to prevent cloud billing leak..."
# Self-stop AWS EC2 instance via metadata and aws-cli
INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
REGION=$(curl -s http://169.254.169.254/latest/meta-data/placement/region)
aws ec2 stop-instances --instance-ids "$INSTANCE_ID" --region "$REGION"
exit 0
fi
sleep 60
done