A Kubernetes-native profiling system for vLLM GPU workers that uses a mutating admission webhook to transparently inject PyTorch profiler instrumentation into vLLM serving pods.
This system enables real-time torch profiling of vLLM model execution without requiring source code modifications or container rebuilds. It works by:
- Intercepting pod creation via Kubernetes mutating admission webhook
- Injecting profiler code via ConfigMap and environment variables
- Auto-loading profiler when Python starts using sitecustomize.py
- Instrumenting vLLM using import hooks to wrap
Worker.execute_modelwith torch.profiler - Capturing traces of CPU+CUDA activity and exporting Chrome trace JSON files
┌─────────────────────────────────────────────────┐
│ User creates Pod with matching label: │
│ - vllm-profiler/enabled=true │
│ Optional annotations for configuration: │
│ - vllm.profiler/ranges="500-503" │
│ - vllm.profiler/export-trace="false" │
└────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Mutating Webhook (webhook.py) │
│ - Checks namespace & label (OR logic) │
│ - Injects: PYTHONPATH=/home/vllm/profiler │
│ - Converts annotations to env vars │
│ - Mounts: sitecustomize.py + config from CM │
└────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Pod starts → Python auto-loads sitecustomize.py │
│ Loads config from YAML & env vars │
│ Installs import hook in sys.meta_path │
└────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ vLLM imports gpu_worker module │
│ Import hook intercepts & wraps execute_model │
│ Supports vLLM >= 0.12 and vLLM 0.11.x │
└────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Profiler runs on configured ranges (e.g. 500-503│
│ Exports: /tmp/trace_rank{rank}_pid{pid}.json │
└─────────────────────────────────────────────────┘
- Kubernetes/OpenShift cluster access
ocorkubectlCLIpodmanordockerfor building images- Cluster admin permissions (for MutatingWebhookConfiguration)
# Deploy webhook and all components
./scripts/deploy.sh
# Or skip image build if using existing image on quay.io
./scripts/deploy.sh --skip-buildThe deployment script will:
- Build and push the webhook container image
- Deploy webhook to
vllm-profilernamespace - Create ConfigMap with profiler code in target namespace
- Generate TLS certificates
- Configure webhook with CA bundle
- Validate deployment
Edit manifests.yaml to configure target namespace and label selectors:
env:
- name: TARGET_NAMESPACE
value: "kserve-e2e-perf"
# Label selector: pod with this label will be instrumented
- name: TARGET_LABELS
value: "vllm-profiler/enabled=true"The webhook uses OR logic when multiple labels are specified (comma-separated) - a pod matching ANY of the specified labels will be profiled. No webhook rebuild needed to change labels.
You can change the target labels without rebuilding the webhook container:
# Update TARGET_LABELS environment variable
oc set env deployment/env-injector -n vllm-profiler \
TARGET_LABELS="vllm-profiler/enabled=true,app=vllm"
# Webhook pod will automatically restart with new configuration
# Verify new configuration:
oc logs -n vllm-profiler deployment/env-injector | grep "Target labels"Create a vLLM pod in the target namespace with the matching label:
# Basic: Pod will automatically be injected with profiler
kubectl run my-vllm-pod \
-n kserve-e2e-perf \
--labels="vllm-profiler/enabled=true" \
--image=vllm/vllm-openai:latest \
-- vllm serve <model-name>Or use a pre-built server config from server_configs/:
# Deploy DeepSeek R1 with profiling enabled
oc apply -f server_configs/deepseek-r1-rhaiis-3.4-EA1.yamlOr use pod annotations for custom profiler configuration:
apiVersion: v1
kind: Pod
metadata:
name: my-vllm-pod
namespace: kserve-e2e-perf
labels:
vllm-profiler/enabled: "true"
annotations:
# Custom profiling ranges (multiple windows)
vllm.profiler/ranges: "500-510,2000-2010"
# Disable trace file export (reduce I/O)
vllm.profiler/export-trace: "false"
# Enable debug logging
vllm.profiler/debug: "true"
spec:
containers:
- name: vllm
image: vllm/vllm-openai:latest
command: ["vllm", "serve", "<model-name>"]The profiler activates after the configured number of model execution calls (default: calls #500-503):
# Watch for profiler output
oc logs -n kserve-e2e-perf <pod-name> -f 2>&1 | grep '\[profiler\]'
# Expected sequence:
# [profiler] vLLM profiler installed - will profile ranges: [(500, 503)]
# [profiler] Starting profiler for range 500-503 (call #500)
# [profiler] Stopping profiler for range 500-503 (call #503)
# [profiler] Exported trace to: /tmp/trace_rank0_pid455_range500-503.json
# Retrieve trace files
oc exec -n kserve-e2e-perf <pod-name> -c kserve-container -- \
bash -c 'tar cf - /tmp/trace_rank*.json' \
| tar xf - -C profiles/<run-name>/ --strip-components=1
# Open in Chrome: navigate to chrome://tracing and load trace.json
# Or use Perfetto: https://ui.perfetto.dev# Remove all webhook resources
TARGET_NAMESPACE=kserve-e2e-perf ./scripts/teardown.sh
# Or skip confirmation prompt
TARGET_NAMESPACE=kserve-e2e-perf ./scripts/teardown.sh --forcevllm-profiler/
├── sitecustomize.py # Profiler import hook (injected into pods)
├── profiler_config.yaml # Default profiler configuration
├── webhook.py # Flask mutating admission webhook
├── manifests.yaml # Kubernetes resources
├── kustomization.yaml # ConfigMap generator
├── Dockerfile # Webhook container image
├── requirements.txt # Python dependencies
├── AGENTS.md # Agent deployment instructions
├── CONFIGURATION_EXAMPLES.md # Configuration guide
├── README.md # This file
├── demo-vllm-profiler.ipynb # Interactive walkthrough notebook
├── scripts/
│ ├── deploy.sh # Full deployment automation
│ ├── teardown.sh # Cleanup script
│ ├── gen-certs.sh # TLS certificate generation
│ ├── patch-ca-bundle.sh # Webhook CA bundle patching
│ └── validate_webhook.sh # Validation tool
├── tests/
│ ├── test-profiler.sh # Standalone profiler testing
│ ├── test-vllm-integration.sh # End-to-end integration test
│ └── test-profiler-features.yaml # Feature testing examples
├── server_configs/ # Pre-built KServe manifests for models
│ ├── deepseek-r1-rhaiis-3.4-EA1.yaml
│ ├── gptoss-vllm-v0.17.0.yaml
│ └── ... # Various model/version combos
├── analysis/ # Profile comparison scripts
├── profiles/ # Collected trace files (local)
└── logs/ # Profiling session logs
Flask-based mutating webhook that:
- Listens for Pod CREATE operations
- Filters by namespace and label selectors (OR logic)
- Extracts profiler configuration from pod annotations
- Converts annotations to environment variables
- Injects
PYTHONPATH=/home/vllm/profilerenvironment variable - Mounts
sitecustomize.pyandprofiler_config.yamlfrom ConfigMap
Python module that:
- Auto-loads when Python starts (via PYTHONPATH)
- Loads configuration from 3 sources (priority order):
- Environment variables (highest priority)
profiler_config.yamlfile- Hardcoded defaults (lowest priority)
- Installs a
sys.meta_pathfinder to intercept vLLM worker module imports - Supports multiple vLLM versions (>= 0.12 via
vllm.v1.worker.gpu_worker, 0.11.x viavllm.worker.worker) - Wraps
Worker.execute_modelwith torch.profiler - Records CPU+CUDA activity for configured call ranges
- Exports Chrome trace JSON file per rank
Configuration is managed via ProfilerConfig class with multi-source support:
Default settings (from profiler_config.yaml):
profiling_ranges: "500-503" # Steady-state profiling (3 forward passes)
activities: "CPU,CUDA"
options:
record_shapes: true
with_stack: true # Enables attributing perf changes to code paths
profile_memory: false
output:
export_chrome_trace: true
file_pattern: "/tmp/trace_rank{rank}_pid{pid}.json"Per-pod override (via annotations):
annotations:
vllm.profiler/ranges: "500-510,2000-2010" # Multiple profiling windows
vllm.profiler/export-trace: "false" # Disable trace export
vllm.profiler/debug: "true" # Enable debug logging
vllm.profiler/activities: "CPU,CUDA"
vllm.profiler/record-shapes: "true"
vllm.profiler/with-stack: "true"
vllm.profiler/memory: "false"
vllm.profiler/output: "/tmp/custom_trace.json"See CONFIGURATION_EXAMPLES.md for comprehensive configuration guide.
Webhook Configuration:
TARGET_NAMESPACE: Namespace to target (default: "kserve-e2e-perf")TARGET_LABELS: Comma-separated label selectors with OR logic (e.g., "key1=val1,key2=val2")TARGET_LABEL_KEY: Legacy single label key (deprecated, use TARGET_LABELS)TARGET_LABEL_VALUE: Legacy single label value (deprecated, use TARGET_LABELS)INJECT_ENV_NAME: Environment variable to inject (default: "PYTHONPATH")INJECT_ENV_VALUE: Environment variable value (default: "/home/vllm/profiler")LOG_LEVEL: Webhook logging level (default: "DEBUG")
Deployment:
CONTAINER_RUNTIME: Container runtime to use (default: "podman")IMAGE_REGISTRY: Image registry (default: "quay.io/mimehta")IMAGE_TAG: Image tag (default: "latest")TARGET_NAMESPACE: Target namespace for ConfigMap (default: "kserve-e2e-perf")
Profiler Configuration (injected via pod annotations or set manually):
VLLM_PROFILER_RANGES: Profiling call ranges (e.g., "500-503" or "500-503,2000-2003")VLLM_PROFILER_ACTIVITIES: Activities to profile (e.g., "CPU,CUDA")VLLM_PROFILER_RECORD_SHAPES: Record tensor shapes (true/false)VLLM_PROFILER_WITH_STACK: Capture stack traces (true/false)VLLM_PROFILER_MEMORY: Profile memory allocations (true/false)VLLM_PROFILER_OUTPUT: Custom trace output file patternVLLM_PROFILER_EXPORT_TRACE: Enable/disable trace export (true/false)VLLM_PROFILER_DEBUG: Enable debug logging (true/false)
Integration Test (Recommended):
Run the complete end-to-end integration test:
# Deploys profiler, creates vLLM pod, runs inference, verifies profiler output
./tests/test-vllm-integration.shThis test:
- Deploys the profiler webhook and ConfigMap
- Creates a vLLM pod with the latest vLLM image
- Waits for vLLM server to be ready (checks /v1/models endpoint)
- Runs vLLM serve with a small test model (facebook/opt-125m)
- Sends a single inference request generating 200 tokens
- Verifies profiler output in the logs
- Cleans up all test resources automatically
Feature Tests:
Test specific profiler features:
# Deploy profiler first
./scripts/deploy.sh
# Run feature tests
oc apply -f tests/test-profiler-features.yaml
# Verify results (check logs, env vars, etc.)
# Cleanup
oc delete -f tests/test-profiler-features.yamlStandalone Test:
Test the profiler standalone with an existing vLLM pod:
# Requires access to a pod running vLLM
./tests/test-profiler.shMethod 1: Update ConfigMap (affects all new pods):
Edit profiler_config.yaml and update the ConfigMap:
profiling_ranges: "2000-2010" # Change profiling window
activities: "CPU,CUDA"
options:
profile_memory: true # Enable memory profiling
record_shapes: trueThen update ConfigMap (no webhook rebuild needed):
# Delete and recreate ConfigMap with updated configuration
oc delete configmap env-injector-files -n kserve-e2e-perf
oc apply -k .
# New pods will automatically get the updated configuration
# Existing pods need to be restarted to pick up changesMethod 2: Per-pod configuration (via annotations):
Add annotations to your pod spec (no ConfigMap update needed):
metadata:
annotations:
vllm.profiler/ranges: "2000-2010"
vllm.profiler/memory: "true"
vllm.profiler/export-trace: "false"Method 3: Test different configurations:
See tests/test-profiler-features.yaml for examples of different configurations.
The webhook supports multiple label selectors - a pod matching ANY of the configured labels will be profiled:
TARGET_LABELS: "vllm-profiler/enabled=true,app=vllm"This eliminates the need to rebuild the webhook when adding new pod types to profile.
Profile multiple non-contiguous call ranges in a single session:
vllm.profiler/ranges: "500-510,2000-2010,5000-5010"This is useful for:
- Capturing warmup vs steady-state performance
- Comparing different phases of model execution
- Reducing profiling overhead while still capturing key intervals
Disable trace file export to reduce I/O overhead in production:
vllm.profiler/export-trace: "false" # Still prints profiler table to logsNo webhook rebuilds needed - configure profiling via:
- ConfigMap (cluster-wide defaults)
- Pod annotations (per-pod overrides)
- Environment variables (highest priority)
Profiling is completely transparent to the application:
- No vLLM source code modifications
- No container rebuilds
- No application downtime
- Automatic instrumentation via import hooks
The profiler automatically detects and instruments:
- vLLM >= 0.12:
vllm.v1.worker.gpu_worker.Worker.execute_model - vLLM 0.11.x:
vllm.worker.worker.Worker.execute_model
These changes can be made without rebuilding the webhook container:
-
Change target labels:
oc set env deployment/env-injector -n vllm-profiler TARGET_LABELS="new,labels,here"
-
Change target namespace:
oc set env deployment/env-injector -n vllm-profiler TARGET_NAMESPACE="new-namespace"
-
Update profiler configuration (ConfigMap):
oc delete configmap env-injector-files -n kserve-e2e-perf oc apply -k . -
Per-pod configuration:
- Just add annotations to your pod spec
These changes require rebuilding and redeploying the webhook:
- Changes to webhook.py logic
- Changes to Python dependencies (requirements.txt)
- Changes to Dockerfile
To rebuild:
./scripts/deploy.sh # Rebuilds container image and redeploysCheck webhook logs:
oc logs -n vllm-profiler deployment/env-injectorVerify webhook configuration:
oc get mutatingwebhookconfiguration env-injector-webhook -o yamlCheck pod has correct environment:
oc get pod <pod-name> -n kserve-e2e-perf -o jsonpath='{.spec.containers[0].env}' | python3 -m json.toolCheck pod has volume mount:
oc get pod <pod-name> -n kserve-e2e-perf -o jsonpath='{.spec.containers[0].volumeMounts}' | python3 -m json.toolCheck pod logs for profiler messages:
oc logs <pod-name> -n kserve-e2e-perf 2>&1 | grep '\[profiler\]'The profiler only activates after reaching the configured call count (default: call #500). Send enough inference requests to reach that threshold.
# Example with vLLM OpenAI-compatible API
curl http://<service-url>:8080/v1/completions \
-H "Content-Type: application/json" \
-d '{"model": "...", "prompt": "Hello", "max_tokens": 200}'Re-generate certs and patch:
bash scripts/gen-certs.sh
bash scripts/patch-ca-bundle.shRun comprehensive validation:
TARGET_NS=kserve-e2e-perf LABEL_KEY="vllm-profiler/enabled" DO_SIMPLE_TEST=1 ./scripts/validate_webhook.shNamespace: vllm-profiler
- Deployment:
env-injector(webhook) - Service:
env-injector(HTTPS on port 443) - ServiceAccount:
env-injector - Secret:
env-injector-certs(TLS certificates)
Target Namespace: kserve-e2e-perf (configurable)
- ConfigMap:
env-injector-files(contains sitecustomize.py and profiler_config.yaml)
Cluster-wide:
- MutatingWebhookConfiguration:
env-injector-webhook
- Webhook requires cluster admin permissions to create MutatingWebhookConfiguration
- Uses self-signed TLS certificates (suitable for development/testing)
- Failure policy is
Ignore- webhook failures won't block pod creation - ConfigMap is mounted read-only into pods
- Profiler code runs with same permissions as vLLM process