Unverified Commit 39d645e5 authored by Jonathan Tong's avatar Jonathan Tong Committed by GitHub
Browse files

docs: migrate Fern docs from fern/ into docs/ (#6206)


Signed-off-by: default avatarJont828 <jt572@cornell.edu>
parent d381e6ff
<!--
SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
# Fault Tolerance Testing
This document describes the test infrastructure for validating Dynamo's fault tolerance mechanisms. The testing framework supports request cancellation, migration, etcd HA, and hardware fault injection scenarios.
## Overview
Dynamo's fault tolerance test suite is located in `tests/fault_tolerance/` and includes:
| Test Category | Location | Purpose |
|---------------|----------|---------|
| Cancellation | `cancellation/` | Request cancellation during in-flight operations |
| Migration | `migration/` | Request migration when workers fail |
| etcd HA | `etcd_ha/` | etcd failover and recovery |
| Hardware | `hardware/` | GPU and network fault injection |
| Deployment | `deploy/` | End-to-end deployment testing |
## Test Directory Structure
```
tests/fault_tolerance/
├── cancellation/
│ ├── test_vllm.py
│ ├── test_trtllm.py
│ ├── test_sglang.py
│ └── utils.py
├── migration/
│ ├── test_vllm.py
│ ├── test_trtllm.py
│ ├── test_sglang.py
│ └── utils.py
├── etcd_ha/
│ ├── test_vllm.py
│ ├── test_trtllm.py
│ ├── test_sglang.py
│ └── utils.py
├── hardware/
│ └── fault_injection_service/
│ ├── api_service/
│ └── agents/
├── deploy/
│ ├── test_deployment.py
│ ├── scenarios.py
│ ├── base_checker.py
│ └── ...
└── client.py
```
## Request Cancellation Tests
Test that in-flight requests can be properly canceled.
### Running Cancellation Tests
```bash
# Run all cancellation tests
pytest tests/fault_tolerance/cancellation/ -v
# Run for specific backend
pytest tests/fault_tolerance/cancellation/test_vllm.py -v
```
### Cancellation Test Utilities
The `cancellation/utils.py` module provides:
#### CancellableRequest
Thread-safe request cancellation via TCP socket manipulation:
```python
from tests.fault_tolerance.cancellation.utils import CancellableRequest
request = CancellableRequest()
# Send request in separate thread
thread = Thread(target=send_request, args=(request,))
thread.start()
# Cancel after some time
time.sleep(1)
request.cancel() # Closes underlying socket
```
#### send_completion_request / send_chat_completion_request
Send cancellable completion requests:
```python
from tests.fault_tolerance.cancellation.utils import (
send_completion_request,
send_chat_completion_request
)
# Non-streaming
response = send_completion_request(
base_url="http://localhost:8000",
model="Qwen/Qwen3-0.6B",
prompt="Hello, world!",
max_tokens=100
)
# Streaming with cancellation
responses = send_chat_completion_request(
base_url="http://localhost:8000",
model="Qwen/Qwen3-0.6B",
messages=[{"role": "user", "content": "Hello!"}],
stream=True,
cancellable_request=request
)
```
#### poll_for_pattern
Wait for specific patterns in logs:
```python
from tests.fault_tolerance.cancellation.utils import poll_for_pattern
# Wait for cancellation confirmation
found = poll_for_pattern(
log_file="/var/log/dynamo/worker.log",
pattern="Request cancelled",
timeout=30,
interval=0.5
)
```
## Migration Tests
Test that requests migrate to healthy workers when failures occur.
### Running Migration Tests
```bash
# Run all migration tests
pytest tests/fault_tolerance/migration/ -v
# Run for specific backend
pytest tests/fault_tolerance/migration/test_vllm.py -v
```
### Migration Test Utilities
The `migration/utils.py` module provides:
- Frontend wrapper with configurable request planes
- Long-running request spawning for migration scenarios
- Health check disabling for controlled testing
### Example Migration Test
```python
def test_migration_on_worker_failure():
# Start deployment with 2 workers
deployment = start_deployment(workers=2)
# Send long-running request
request_thread = spawn_long_request(max_tokens=1000)
# Kill one worker mid-generation
kill_worker(deployment.workers[0])
# Verify request completes on remaining worker
response = request_thread.join()
assert response.status_code == 200
assert len(response.tokens) > 0
```
## etcd HA Tests
Test system behavior during etcd failures and recovery.
### Running etcd HA Tests
```bash
pytest tests/fault_tolerance/etcd_ha/ -v
```
### Test Scenarios
- **Leader failover**: etcd leader node fails, cluster elects new leader
- **Network partition**: etcd node becomes unreachable
- **Recovery**: System recovers after etcd becomes available
## Hardware Fault Injection
The fault injection service enables testing under simulated hardware failures.
### Fault Injection Service
Located at `tests/fault_tolerance/hardware/fault_injection_service/`, this FastAPI service orchestrates fault injection:
```bash
# Start the fault injection service
cd tests/fault_tolerance/hardware/fault_injection_service
python -m api_service.main
```
### Supported Fault Types
#### GPU Faults
| Fault Type | Description |
|------------|-------------|
| `XID_ERROR` | Simulate GPU XID error (various codes) |
| `THROTTLE` | GPU thermal throttling |
| `MEMORY_PRESSURE` | GPU memory exhaustion |
| `OVERHEAT` | GPU overheating condition |
| `COMPUTE_OVERLOAD` | GPU compute saturation |
#### Network Faults
| Fault Type | Description |
|------------|-------------|
| `FRONTEND_WORKER` | Partition between frontend and workers |
| `WORKER_NATS` | Partition between workers and NATS |
| `WORKER_WORKER` | Partition between workers |
| `CUSTOM` | Custom network partition |
### Fault Injection API
#### Inject GPU Fault
```bash
curl -X POST http://localhost:8080/api/v1/faults/gpu/inject \
-H "Content-Type: application/json" \
-d '{
"target_pod": "vllm-worker-0",
"fault_type": "XID_ERROR",
"severity": "HIGH"
}'
```
#### Inject Specific XID Error
```bash
# Inject XID 79 (GPU memory page fault)
curl -X POST http://localhost:8080/api/v1/faults/gpu/inject/xid-79 \
-H "Content-Type: application/json" \
-d '{"target_pod": "vllm-worker-0"}'
```
Supported XID codes: 43, 48, 74, 79, 94, 95, 119, 120
#### Inject Network Partition
```bash
curl -X POST http://localhost:8080/api/v1/faults/network/inject \
-H "Content-Type: application/json" \
-d '{
"partition_type": "FRONTEND_WORKER",
"duration_seconds": 30
}'
```
#### Recover from Fault
```bash
curl -X POST http://localhost:8080/api/v1/faults/{fault_id}/recover
```
#### List Active Faults
```bash
curl http://localhost:8080/api/v1/faults
```
### GPU Fault Injector Agent
The GPU fault injector runs as a DaemonSet on worker nodes:
```yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: gpu-fault-injector
spec:
selector:
matchLabels:
app: gpu-fault-injector
template:
spec:
containers:
- name: agent
image: dynamo/gpu-fault-injector:latest
securityContext:
privileged: true
volumeMounts:
- name: dev
mountPath: /dev
```
The agent injects fake XID messages via `/dev/kmsg` to trigger NVSentinel detection.
## Deployment Testing Framework
The `deploy/` directory contains an end-to-end testing framework.
### Test Phases
Tests run through three phases:
| Phase | Description |
|-------|-------------|
| `STANDARD` | Baseline performance under normal conditions |
| `OVERFLOW` | System behavior during fault/overload |
| `RECOVERY` | System recovery after fault resolution |
### Scenario Configuration
Define test scenarios in `scenarios.py`:
```python
from tests.fault_tolerance.deploy.scenarios import Scenario, Load, Failure
scenario = Scenario(
name="worker_failure_migration",
backend="vllm",
load=Load(
clients=10,
requests_per_client=100,
max_tokens=256
),
failure=Failure(
type="pod_kill",
target="vllm-worker-0",
trigger_after_requests=50
)
)
```
### Running Deployment Tests
```bash
# Run all deployment tests
pytest tests/fault_tolerance/deploy/test_deployment.py -v
# Run specific scenario
pytest tests/fault_tolerance/deploy/test_deployment.py::test_worker_failure -v
```
### Validation Checkers
The framework includes pluggable validators:
```python
from tests.fault_tolerance.deploy.base_checker import BaseChecker, ValidationContext
class MigrationChecker(BaseChecker):
def check(self, context: ValidationContext) -> bool:
# Verify migrations occurred
migrations = context.metrics.get("migrations_total", 0)
return migrations > 0
```
### Results Parsing
Parse test results for analysis:
```python
from tests.fault_tolerance.deploy.parse_results import process_overflow_recovery_test
results = process_overflow_recovery_test(log_dir="/path/to/logs")
print(f"Success rate: {results['success_rate']}")
print(f"P99 latency: {results['p99_latency_ms']}ms")
```
## Client Utilities
The `client.py` module provides shared client functionality:
### Multi-Threaded Load Generation
```python
from tests.fault_tolerance.client import client
# Generate load with multiple clients
results = client(
base_url="http://localhost:8000",
num_clients=10,
requests_per_client=100,
model="Qwen/Qwen3-0.6B",
max_tokens=256,
log_dir="/tmp/test_logs"
)
```
### Request Options
| Parameter | Description |
|-----------|-------------|
| `base_url` | Frontend URL |
| `num_clients` | Number of concurrent clients |
| `requests_per_client` | Requests per client |
| `model` | Model name |
| `max_tokens` | Max tokens per request |
| `log_dir` | Directory for client logs |
| `endpoint` | `completions` or `chat/completions` |
## Running the Full Test Suite
### Prerequisites
1. Kubernetes cluster with GPU nodes
2. Dynamo deployment
3. etcd cluster (for HA tests)
4. Fault injection service (for hardware tests)
### Environment Setup
```bash
export KUBECONFIG=/path/to/kubeconfig
export DYNAMO_NAMESPACE=dynamo-test
export FRONTEND_URL=http://localhost:8000
```
### Run All Tests
```bash
# Install test dependencies
pip install pytest pytest-asyncio
# Run all fault tolerance tests
pytest tests/fault_tolerance/ -v --tb=short
# Run with specific markers
pytest tests/fault_tolerance/ -v -m "not slow"
```
### Test Markers
| Marker | Description |
|--------|-------------|
| `slow` | Long-running tests (> 5 minutes) |
| `gpu` | Requires GPU resources |
| `k8s` | Requires Kubernetes cluster |
| `etcd_ha` | Requires multi-node etcd |
## Best Practices
### 1. Isolate Test Environments
Run fault tolerance tests in dedicated namespaces:
```bash
kubectl create namespace dynamo-fault-test
```
### 2. Clean Up After Tests
Ensure fault injection is recovered:
```bash
# List and recover all active faults
curl http://localhost:8080/api/v1/faults | jq -r '.[].id' | \
xargs -I {} curl -X POST http://localhost:8080/api/v1/faults/{}/recover
```
### 3. Collect Logs
Preserve logs for debugging:
```bash
pytest tests/fault_tolerance/ -v \
--log-dir=/tmp/fault_test_logs \
--capture=no
```
### 4. Monitor During Tests
Watch system state during tests:
```bash
# Terminal 1: Watch pods
watch kubectl get pods -n dynamo-test
# Terminal 2: Watch metrics
watch 'curl -s localhost:8000/metrics | grep -E "(migration|rejection)"'
```
## Related Documentation
- [Request Migration](request_migration.md) - Migration implementation details
- [Request Cancellation](request_cancellation.md) - Cancellation implementation
- [Health Checks](../observability/health-checks.md) - Health monitoring
- [Metrics](../observability/metrics.md) - Available metrics for monitoring
<!--
SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0
-->
# Disaggregated Serving Guide
[AIConfigurator](https://github.com/ai-dynamo/aiconfigurator/tree/main) is a performance optimization tool that helps you find the optimal configuration for deploying LLMs with Dynamo. It automatically determines the best number of prefill and decode workers, parallelism settings, and deployment parameters to meet your SLA targets while maximizing throughput.
## Why Use AIConfigurator?
When deploying LLMs with Dynamo, you need to make several critical decisions:
- **Aggregated vs Disaggregated**: Which architecture gives better performance for your workload?
- **Worker Configuration**: How many prefill and decode workers to deploy?
- **Parallelism Settings**: What tensor/pipeline parallel configuration to use?
- **SLA Compliance**: How to meet your TTFT and TPOT targets?
AIConfigurator answers these questions in seconds, providing:
- Recommended configurations that meet your SLA requirements
- Ready-to-deploy Dynamo configuration files (including Kubernetes manifests)
- Performance comparisons between different deployment strategies
- Up to 1.7x better throughput compared to manual configuration
### End-to-End Workflow
![AIConfigurator end-to-end workflow](../../images/e2e_workflow.svg)
### Aggregated vs Disaggregated Architecture
AIConfigurator evaluates two deployment architectures and recommends the best one for your workload:
![Aggregated vs Disaggregated architecture comparison](../../images/arch_comparison.svg)
### When to Use Each Architecture
![Decision flowchart for choosing aggregated vs disaggregated](../../images/decision_flowchart.svg)
## Quick Start
```bash
# Install
pip3 install aiconfigurator
# Find optimal configuration for vLLM backend
aiconfigurator cli default \
--model_path Qwen/Qwen3-32B-FP8 \
--total_gpus 8 \
--system h200_sxm \
--backend vllm \
--backend_version 0.12.0 \
--isl 4000 \
--osl 500 \
--ttft 600 \
--tpot 16.67 \
--save_dir ./results_vllm
# Deploy on Kubernetes
kubectl apply -f ./results_vllm/agg/top1/agg/k8s_deploy.yaml
```
## Complete Walkthrough: vLLM on H200
This section walks through a validated example deploying Qwen3-32B-FP8 on 8× H200 GPUs using vLLM.
### Step 1: Run AIConfigurator
```bash
aiconfigurator cli default \
--model_path Qwen/Qwen3-32B-FP8 \
--system h200_sxm \
--total_gpus 8 \
--isl 4000 \
--osl 500 \
--ttft 600 \
--tpot 16.67 \
--backend vllm \
--backend_version 0.12.0 \
--save_dir ./results_vllm
```
**Parameters explained:**
- `--model_path`: HuggingFace model ID or local path (e.g., `Qwen/Qwen3-32B-FP8`)
- `--system`: GPU system type (`h200_sxm`, `h100_sxm`, `a100_sxm`)
- `--total_gpus`: Number of GPUs available for deployment
- `--isl` / `--osl`: Input/Output sequence lengths in tokens
- `--ttft` / `--tpot`: SLA targets - Time To First Token (ms) and Time Per Output Token (ms)
- `--backend`: Inference backend (`vllm`, `trtllm`, or `sglang`)
- `--backend_version`: Backend version (e.g., `0.12.0` for vLLM)
- `--save_dir`: Directory to save generated deployment configs
### Step 2: Review the Results
AIConfigurator outputs a comparison of aggregated vs disaggregated deployment strategies:
```text
********************************************************************************
* Dynamo aiconfigurator Final Results *
********************************************************************************
----------------------------------------------------------------------------
Input Configuration & SLA Target:
Model: Qwen/Qwen3-32B-FP8 (is_moe: False)
Total GPUs: 8
Best Experiment Chosen: disagg at 521.77 tokens/s/gpu
----------------------------------------------------------------------------
Overall Best Configuration:
- Best Throughput: 4,174.16 tokens/s
- Per-GPU Throughput: 521.77 tokens/s/gpu
- Per-User Throughput: 76.96 tokens/s/user
- TTFT: 388.11ms
- TPOT: 12.99ms
----------------------------------------------------------------------------
```
AIC evaluates both aggregated and disaggregated architectures and outputs ranked configurations for each:
```text
agg Top Configurations: (Sorted by tokens/s/gpu)
+------+--------------+---------------+--------+-----------------+-------------+-------------------+----------+----------+----+
| Rank | tokens/s/gpu | tokens/s/user | TTFT | request_latency | concurrency | total_gpus (used) | replicas | parallel | bs |
+------+--------------+---------------+--------+-----------------+-------------+-------------------+----------+----------+----+
| 1 | 397.31 | 60.66 | 509.14 | 8734.68 | 56 (=14x4) | 8 (8=4x2) | 4 | tp2pp1 | 14 |
| 2 | 349.90 | 60.98 | 412.58 | 8596.28 | 48 (=24x2) | 8 (8=2x4) | 2 | tp4pp1 | 24 |
| 3 | 235.62 | 62.71 | 482.57 | 8439.41 | 32 (=32x1) | 8 (8=1x8) | 1 | tp8pp1 | 32 |
+------+--------------+---------------+--------+-----------------+-------------+-------------------+----------+----------+----+
disagg Top Configurations: (Sorted by tokens/s/gpu)
+------+--------------+---------------+--------+-----------------+-------------+-------------------+----------+-------------+----------+
| Rank | tokens/s/gpu | tokens/s/user | TTFT | request_latency | concurrency | total_gpus (used) | replicas | (p)parallel | (d)parallel |
+------+--------------+---------------+--------+-----------------+-------------+-------------------+----------+-------------+----------+
| 1 | 521.77 | 76.96 | 388.11 | 6871.61 | 60 (=60x1) | 8 (8=1x8) | 1 | tp2pp1 | tp4pp1 |
| 2 | 521.77 | 63.29 | 388.11 | 8272.31 | 80 (=40x2) | 8 (8=2x4) | 2 | tp2pp1 | tp2pp1 |
| 3 | 260.89 | 62.81 | 388.11 | 8332.18 | 42 (=42x1) | 8 (8=1x8) | 1 | tp2pp1 | tp1pp1 |
+------+--------------+---------------+--------+-----------------+-------------+-------------------+----------+-------------+----------+
```
**Reading the output:**
- **tokens/s/gpu**: Overall throughput efficiency — higher is better
- **tokens/s/user**: Per-request generation speed (inverse of TPOT)
- **TTFT**: Predicted time to first token
- **concurrency**: Total concurrent requests across all replicas (e.g., `56 (=14x4)` means batch size 14 × 4 replicas)
- **agg Rank 1** recommends TP2 with 4 replicas — simpler to deploy
- **disagg Rank 1** recommends 2 prefill workers (TP2) + 1 decode worker (TP4) — higher throughput but requires RDMA
### Step 3: Deploy on Kubernetes
The `--save_dir` generates ready-to-use Kubernetes manifests:
```
results_vllm/
├── agg/
│ └── top1/
│ └── agg/
│ ├── k8s_deploy.yaml # Kubernetes DynamoGraphDeployment
│ └── agg_config.yaml # Engine configuration
├── disagg/
│ └── top1/
│ └── disagg/
│ ├── k8s_deploy.yaml
│ ├── prefill_config.yaml
│ └── decode_config.yaml
└── pareto_frontier.png
```
#### Prerequisites
Before deploying, ensure you have:
1. **HuggingFace Token Secret** (for gated models):
```bash
kubectl create secret generic hf-token-secret \
-n your-namespace \
--from-literal=HF_TOKEN="your-huggingface-token"
```
2. **Model Cache PVC** (recommended for faster restarts):
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: model-cache
namespace: your-namespace
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 100Gi
```
#### Deploy the Configuration
The generated `k8s_deploy.yaml` provides a starting point. You'll typically need to customize it for your environment:
```bash
kubectl apply -f ./results_vllm/agg/top1/agg/k8s_deploy.yaml
```
**Complete deployment example** with model cache and production settings:
```yaml
apiVersion: nvidia.com/v1alpha1
kind: DynamoGraphDeployment
metadata:
name: dynamo-agg
namespace: your-namespace
spec:
backendFramework: vllm
pvcs:
- name: model-cache
create: false # Use existing PVC
services:
Frontend:
componentType: frontend
replicas: 1
volumeMounts:
- name: model-cache
mountPoint: /opt/models
envs:
- name: HF_HOME
value: /opt/models
extraPodSpec:
mainContainer:
image: nvcr.io/nvidia/ai-dynamo/vllm-runtime:0.8.0
imagePullPolicy: IfNotPresent
VLLMWorker:
envFromSecret: hf-token-secret
componentType: worker
replicas: 4
resources:
limits:
gpu: "2"
sharedMemory:
size: 16Gi # Required for vLLM
volumeMounts:
- name: model-cache
mountPoint: /opt/models
envs:
- name: HF_HOME
value: /opt/models
extraPodSpec:
mainContainer:
image: nvcr.io/nvidia/ai-dynamo/vllm-runtime:0.8.0
workingDir: /workspace
imagePullPolicy: IfNotPresent
command:
- python3
- -m
- dynamo.vllm
args:
- --model
- "Qwen/Qwen3-32B-FP8"
- "--no-enable-prefix-caching"
- "--tensor-parallel-size"
- "2"
- "--pipeline-parallel-size"
- "1"
- "--data-parallel-size"
- "1"
- "--kv-cache-dtype"
- "fp8"
- "--max-model-len"
- "6000"
- "--max-num-seqs"
- "1024"
```
**Key deployment settings:**
| Setting | Purpose | Notes |
|---------|---------|-------|
| `backendFramework: vllm` | Tells Dynamo which runtime to use | Required at spec level |
| `pvcs` + `volumeMounts` | Caches model weights across restarts | Mount at `/opt/models` (not `/root/`) |
| `HF_HOME` env var | Points HuggingFace to cache location | Must match `mountPoint` |
| `sharedMemory.size: 16Gi` | IPC memory for vLLM | 16Gi for vLLM, 80Gi for TRT-LLM |
| `envFromSecret` | Injects HF_TOKEN | Required for gated models |
### Step 4: Validate with AIPerf
After deployment, validate the predictions against actual performance using [AIPerf](https://github.com/ai-dynamo/aiperf).
> **Tip**: Run AIPerf **inside the cluster** to avoid network latency affecting measurements. Use a Kubernetes Job:
#### Deriving AIPerf Parameters from AIC Output
To use AIPerf to benchmark an AIC-recommended configuration, you'll need to translate AIC parameters into AIPerf profiling arguments (we are working to automate this):
![AIC-to-AIPerf parameter mapping](../../images/param_mapping.svg)
| AIC Output | AIPerf Parameter | Notes |
|------------|-----------------|-------|
| `concurrency: 56 (=14x4)` | `--concurrency 56` | Use total concurrency when benchmarking via the frontend |
| ISL/OSL targets | `--isl 4000 --osl 500` | Match your AIC inputs |
| - | `--num-requests 800` | Use `concurrency × 40` minimum for statistical stability |
| - | `--extra-inputs "ignore_eos:true"` | Ensures exact OSL tokens generated |
> **Note on concurrency**: AIC reports concurrency as `total (=bs × replicas)`. When benchmarking through the frontend (which routes to all replicas), use the total value. If benchmarking a single replica directly, use the per-replica `bs` value instead.
```yaml
apiVersion: batch/v1
kind: Job
metadata:
name: aiperf-benchmark
namespace: your-namespace
spec:
template:
spec:
restartPolicy: Never
containers:
- name: aiperf
image: python:3.10
command:
- /bin/bash
- -c
- |
pip install aiperf
aiperf profile \
-m Qwen/Qwen3-32B-FP8 \
--endpoint-type chat \
-u http://dynamo-agg-frontend:8000 \
--isl 4000 --isl-stddev 0 \
--osl 500 --osl-stddev 0 \
--num-requests 800 \
--concurrency 56 \
--streaming \
--extra-inputs "ignore_eos:true" \
--num-warmup-requests 40 \
--ui-type simple
```
```bash
kubectl apply -f aiperf-job.yaml
kubectl logs -f -l job-name=aiperf-benchmark
```
**Validated results** (Qwen3-32B-FP8, 8× H200, TP2×4 replicas, aggregated):
| Metric | AIC Prediction | Actual (avg) | Status |
|--------|---------------|--------------|--------|
| TTFT (ms) | 509 | 209 | Better than target |
| ITL/TPOT (ms) | 16.49 | 15.06 | Within 10% |
| Throughput (req/s) | ~6.3 | 6.9 | Within 10% |
| Total Output TPS | ~3,178 | 3,462 | Within 10% |
> **Note**: Actual throughput typically reaches ~85-90% of AIC predictions, with ITL/TPOT being the most accurate metric. Expect some variance between benchmark runs; running multiple times is recommended. Enable prefix caching (`--enable-prefix-caching`) for additional TTFT improvements with repeated prompts.
## Fine-Tuning Your Deployment
AIConfigurator provides a strong starting point. Here's how to iterate for production:
### Adjusting for Actual Workload
If your real workload differs from the benchmark parameters:
```bash
# For longer outputs (chat/code generation):
# increase OSL, relax TTFT target
aiconfigurator cli default \
--model_path Qwen/Qwen3-32B-FP8 \
--total_gpus 8 \
--system h200_sxm \
--backend vllm \
--backend_version 0.12.0 \
--isl 2000 \
--osl 2000 \
--ttft 1000 \
--tpot 10 \
--save_dir ./results_long_output
```
### Exploring Alternative Configurations
Use `exp` mode to compare custom configurations:
```yaml
# custom_exp.yaml
exps:
- exp_tp2
- exp_tp4
exp_tp2:
mode: "patch"
serving_mode: "agg"
model_path: "Qwen/Qwen3-32B-FP8"
total_gpus: 8
system_name: "h200_sxm"
backend_name: "vllm"
backend_version: "0.12.0"
isl: 4000
osl: 500
ttft: 600
tpot: 16.67
config:
agg_worker_config:
tp_list: [2]
exp_tp4:
mode: "patch"
serving_mode: "agg"
model_path: "Qwen/Qwen3-32B-FP8"
total_gpus: 8
system_name: "h200_sxm"
backend_name: "vllm"
backend_version: "0.12.0"
isl: 4000
osl: 500
ttft: 600
tpot: 16.67
config:
agg_worker_config:
tp_list: [4]
```
```bash
aiconfigurator cli exp --yaml_path custom_exp.yaml --save_dir ./results_custom
```
> **Critical**: Disaggregated deployments **require RDMA** for KV cache transfer. Without RDMA, performance degrades by **40x** (TTFT increases from 355ms to 10+ seconds). See the Disaggregated Deployment section below.
### Deploying Disaggregated (RDMA Required)
Disaggregated deployments transfer KV cache between prefill and decode workers. **Without RDMA, this transfer becomes a severe bottleneck**, causing 40x performance degradation.
#### Prerequisites for Disaggregated
1. **RDMA-capable network** (InfiniBand or RoCE)
2. **RDMA device plugin** installed on the cluster (provides `rdma/ib` resources)
3. **ETCD and NATS** deployed (for coordination)
#### Disaggregated DGD with RDMA
```yaml
apiVersion: nvidia.com/v1alpha1
kind: DynamoGraphDeployment
metadata:
name: dynamo-disagg
namespace: your-namespace
spec:
backendFramework: vllm
pvcs:
- name: model-cache
create: false
services:
Frontend:
componentType: frontend
replicas: 1
volumeMounts:
- name: model-cache
mountPoint: /opt/models
envs:
- name: HF_HOME
value: /opt/models
extraPodSpec:
mainContainer:
image: nvcr.io/nvidia/ai-dynamo/vllm-runtime:0.8.0
imagePullPolicy: IfNotPresent
VLLMPrefillWorker:
envFromSecret: hf-token-secret
componentType: worker
subComponentType: prefill
replicas: 2
resources:
limits:
gpu: "2"
sharedMemory:
size: 16Gi
volumeMounts:
- name: model-cache
mountPoint: /opt/models
envs:
- name: HF_HOME
value: /opt/models
- name: UCX_TLS
value: "rc_x,rc,dc_x,dc,cuda_copy,cuda_ipc" # Enable RDMA transports
- name: UCX_RNDV_SCHEME
value: "get_zcopy"
- name: UCX_RNDV_THRESH
value: "0"
extraPodSpec:
mainContainer:
image: nvcr.io/nvidia/ai-dynamo/vllm-runtime:0.8.0
workingDir: /workspace
imagePullPolicy: IfNotPresent
securityContext:
capabilities:
add: ["IPC_LOCK"] # Required for RDMA memory registration
resources:
limits:
rdma/ib: "2" # Request RDMA resources
requests:
rdma/ib: "2"
command: ["python3", "-m", "dynamo.vllm"]
args:
- --model
- "Qwen/Qwen3-32B-FP8"
- "--tensor-parallel-size"
- "2"
- "--kv-cache-dtype"
- "fp8"
- "--max-num-seqs"
- "1" # Prefill workers use batch size 1
- --is-prefill-worker
VLLMDecodeWorker:
envFromSecret: hf-token-secret
componentType: worker
subComponentType: decode
replicas: 1
resources:
limits:
gpu: "4"
sharedMemory:
size: 16Gi
volumeMounts:
- name: model-cache
mountPoint: /opt/models
envs:
- name: HF_HOME
value: /opt/models
- name: UCX_TLS
value: "rc_x,rc,dc_x,dc,cuda_copy,cuda_ipc"
- name: UCX_RNDV_SCHEME
value: "get_zcopy"
- name: UCX_RNDV_THRESH
value: "0"
extraPodSpec:
mainContainer:
image: nvcr.io/nvidia/ai-dynamo/vllm-runtime:0.8.0
workingDir: /workspace
imagePullPolicy: IfNotPresent
securityContext:
capabilities:
add: ["IPC_LOCK"]
resources:
limits:
rdma/ib: "4"
requests:
rdma/ib: "4"
command: ["python3", "-m", "dynamo.vllm"]
args:
- --model
- "Qwen/Qwen3-32B-FP8"
- "--tensor-parallel-size"
- "4"
- "--kv-cache-dtype"
- "fp8"
- "--max-num-seqs"
- "1024" # Decode workers handle high concurrency
- --is-decode-worker
```
**Critical RDMA settings:**
| Setting | Purpose |
|---------|---------|
| `rdma/ib: "N"` | Request N RDMA resources (match TP size) |
| `IPC_LOCK` capability | Required for RDMA memory registration |
| `UCX_TLS` env var | Enables RDMA transports (rc_x, dc_x) |
| `UCX_RNDV_SCHEME=get_zcopy` | Zero-copy RDMA transfers |
#### Verifying RDMA is Active
After deployment, check the worker logs for UCX initialization:
```bash
kubectl logs <prefill-worker-pod> | grep -i "UCX\|NIXL"
```
You should see:
```
NIXL INFO Backend UCX was instantiated
```
If you see only TCP transports, RDMA is not active - check your RDMA device plugin and resource requests.
### Tuning vLLM-Specific Parameters
Override vLLM engine parameters with `--generator-set`:
```bash
aiconfigurator cli default \
--model_path Qwen/Qwen3-32B-FP8 \
--total_gpus 8 \
--system h200_sxm \
--backend vllm \
--backend_version 0.12.0 \
--isl 4000 --osl 500 \
--ttft 600 --tpot 16.67 \
--save_dir ./results_tuned \
--generator-set Workers.agg.kv_cache_free_gpu_memory_fraction=0.85 \
--generator-set Workers.agg.max_num_seqs=2048
```
Run `aiconfigurator cli default --generator-help` to see all available parameters.
### Prefix Caching Considerations
For workloads with repeated prefixes (e.g., system prompts):
- **Enable prefix caching** when you have high prefix hit rates
- **Disable prefix caching** (`--no-enable-prefix-caching`) for diverse prompts
AIConfigurator's default predictions assume no prefix caching. Enable it post-deployment if your workload benefits.
## Supported Configurations
### Backends and Versions
| Backend | Versions | Status |
|---------|----------|--------|
| TensorRT-LLM | 1.0.0rc3, 1.2.0rc5 | Production |
| vLLM | 0.12.0 | Production |
| SGLang | 0.5.6.post2 | Production |
### Systems
| GPU System | TensorRT-LLM | vLLM | SGLang |
|------------|--------------|------|--------|
| H200 SXM | Yes | Yes | Yes |
| H100 SXM | Yes | Yes | Yes |
| A100 SXM | Yes | Yes | -- |
| B200 SXM | Yes | -- | Yes |
| GB200 SXM | Yes | -- | -- |
### Models
- **Dense**: GPT, LLAMA2/3, QWEN2.5/3
- **MoE**: Mixtral, DEEPSEEK_V3
## Common Use Cases
```bash
# Strict latency SLAs (real-time chat)
aiconfigurator cli default \
--model_path meta-llama/Llama-3.1-70B \
--total_gpus 16 \
--system h200_sxm \
--backend vllm \
--backend_version 0.12.0 \
--ttft 200 --tpot 8
# High throughput (batch processing)
aiconfigurator cli default \
--model_path Qwen/Qwen3-32B-FP8 \
--total_gpus 32 \
--system h200_sxm \
--backend trtllm \
--ttft 2000 --tpot 50
# Request latency constraint (end-to-end SLA)
aiconfigurator cli default \
--model_path Qwen/Qwen3-32B-FP8 \
--total_gpus 16 \
--system h200_sxm \
--backend vllm \
--backend_version 0.12.0 \
--request_latency 12000 \
--isl 4000 --osl 500
```
## Additional Options
```bash
# Web interface for interactive exploration
pip3 install aiconfigurator[webapp]
aiconfigurator webapp # Visit http://127.0.0.1:7860
# Quick config generation (no parameter sweep)
aiconfigurator cli generate \
--model_path Qwen/Qwen3-32B-FP8 \
--total_gpus 8 \
--system h200_sxm \
--backend vllm
# Check model/system support
aiconfigurator cli support \
--model_path Qwen/Qwen3-32B-FP8 \
--system h200_sxm \
--backend vllm
```
## Troubleshooting
### AIConfigurator Issues
**Model not found**: Use the full HuggingFace path (e.g., `Qwen/Qwen3-32B-FP8` not `QWEN3_32B`)
**Backend version mismatch**: Check supported versions with `aiconfigurator cli support --model_path <model> --system <system> --backend <backend>`
### Deployment Issues
**Pods crash with "Permission denied" on cache directory**:
- Mount the PVC at `/opt/models` instead of `/root/.cache/huggingface`
- Set `HF_HOME=/opt/models` environment variable
- Ensure the PVC has `ReadWriteMany` access mode
**Workers stuck in CrashLoopBackOff**:
- Check logs: `kubectl logs <pod-name> --previous`
- Verify `sharedMemory.size` is set (16Gi for vLLM, 80Gi for TRT-LLM)
- Ensure HuggingFace token secret exists and is named correctly
**Model download slow on every restart**:
- Add PVC for model caching (see deployment example above)
- Verify `volumeMounts` and `HF_HOME` are configured on workers
**"Context stopped or killed" errors (disaggregated only)**:
- Deploy ETCD and NATS infrastructure (required for KV cache transfer)
- See [Dynamo Kubernetes Guide](/docs/kubernetes/README.md) for platform setup
### Performance Issues
**OOM errors**: Reduce `--max-num-seqs` or increase tensor parallelism
**Performance below predictions**:
- Verify warmup requests are sufficient (40+ recommended)
- Check for competing workloads on the cluster
- Ensure KV cache memory fraction is optimized
- Run benchmarks from inside the cluster to eliminate network latency
**Disaggregated TTFT extremely high (10+ seconds)**:
This is almost always caused by **missing RDMA configuration**. Without RDMA, KV cache transfer falls back to TCP and becomes a severe bottleneck.
To diagnose:
```bash
# Check if RDMA resources are allocated
kubectl get pod <worker-pod> -o yaml | grep -A5 "resources:"
# Check UCX transport in logs
kubectl logs <worker-pod> | grep -i "UCX\|transport"
```
To fix:
1. Ensure your cluster has RDMA device plugin installed
2. Add `rdma/ib` resource requests to worker pods
3. Add `IPC_LOCK` capability to security context
4. Add UCX environment variables (see Disaggregated Deployment section)
**Disaggregated working but throughput lower than aggregated**:
For balanced workloads (ISL/OSL ratio between 2:1 and 10:1), aggregated is often better. Disaggregated shines for:
- Very long inputs (ISL > 8000) with short outputs
- Workloads needing independent prefill/decode scaling
## Learn More
- [AIConfigurator CLI Guide](https://github.com/ai-dynamo/aiconfigurator/blob/main/docs/cli_user_guide.md)
- [Dynamo Deployment Guide](https://github.com/ai-dynamo/aiconfigurator/blob/main/docs/dynamo_deployment_guide.md)
- [Dynamo Installation Guide](/docs/kubernetes/installation_guide.md)
- [Benchmarking Guide](/docs/benchmarks/benchmarking.md)
<!--
SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES.
All rights reserved.
SPDX-License-Identifier: Apache-2.0
-->
# LoRA Adapters
LoRA (Low-Rank Adaptation) enables efficient fine-tuning and serving of specialized model variants without duplicating full model weights. Dynamo provides built-in support for dynamic LoRA adapter loading, caching, and inference routing.
## Backend Support
| Backend | Status | Notes |
|---------|--------|-------|
| vLLM | ✅ | Full support including KV-aware routing |
| SGLang | 🚧 | In progress |
| TensorRT-LLM | ❌ | Not yet supported |
See the [Feature Matrix](../../reference/feature-matrix.md) for full compatibility details.
## Overview
Dynamo's LoRA implementation provides:
- **Dynamic loading**: Load and unload LoRA adapters at runtime without restarting workers
- **Multiple sources**: Load from local filesystem (`file://`), S3-compatible storage (`s3://`), or Hugging Face Hub (`hf://`)
- **Automatic caching**: Downloaded adapters are cached locally to avoid repeated downloads
- **Discovery integration**: Loaded LoRAs are automatically registered and discoverable via `/v1/models`
- **KV-aware routing**: Route requests to workers with the appropriate LoRA loaded
- **Kubernetes native**: Declarative LoRA management via the `DynamoModel` CRD
### Architecture
```text
┌─────────────────────────────────────────────────────────────────┐
│ LoRA Architecture │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Frontend │────▶│ Router │────▶│ Workers │ │
│ │ /v1/models │ │ LoRA-aware │ │ LoRA-loaded │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ LoRA Manager │ │
│ │ ┌───────────┐ ┌─────────────┐ │ │
│ │ │ Downloader│ │ Cache │ │ │
│ │ └───────────┘ └─────────────┘ │ │
│ └─────────────────────────────────┘ │
│ │ │
│ ┌───────────────────┼───────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌─────────┐│
│ │ file:// │ │ s3:// │ │ hf:// ││
│ │ Local │ │ S3/MinIO │ │(custom) ││
│ └────────────┘ └────────────┘ └─────────┘│
└─────────────────────────────────────────────────────────────────┘
```
The LoRA system consists of:
- **Rust Core** (`lib/llm/src/lora/`): High-performance downloading, caching, and validation
- **Python Manager** (`components/src/dynamo/common/lora/`): Extensible wrapper with custom source support
- **Worker Handlers** (`components/src/dynamo/vllm/handlers.py`): Load/unload API and inference integration
## Quick Start
### Prerequisites
- Dynamo installed with vLLM support
- For S3 sources: AWS credentials configured
- A LoRA adapter compatible with your base model
### Local Development
**1. Start Dynamo with LoRA support:**
```bash
# Start vLLM worker with LoRA flags
DYN_SYSTEM_ENABLED=true DYN_SYSTEM_PORT=8081 \
python -m dynamo.vllm --model Qwen/Qwen3-0.6B --enforce-eager \
--connector none \
--enable-lora \
--max-lora-rank 64
```
**2. Load a LoRA adapter:**
```bash
curl -X POST http://localhost:8081/v1/loras \
-H "Content-Type: application/json" \
-d '{
"lora_name": "my-lora",
"source": {
"uri": "file:///path/to/my-lora"
}
}'
```
**3. Run inference with the LoRA:**
```bash
curl -X POST http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "my-lora",
"messages": [{"role": "user", "content": "Hello!"}],
"max_tokens": 100
}'
```
### S3-Compatible Storage
For production deployments, store LoRA adapters in S3-compatible storage:
```bash
# Configure S3 credentials
export AWS_ACCESS_KEY_ID=your-access-key
export AWS_SECRET_ACCESS_KEY=your-secret-key
export AWS_ENDPOINT=http://minio:9000 # For MinIO
export AWS_REGION=us-east-1
# Load LoRA from S3
curl -X POST http://localhost:8081/v1/loras \
-H "Content-Type: application/json" \
-d '{
"lora_name": "customer-support-lora",
"source": {
"uri": "s3://my-loras/customer-support-v1"
}
}'
```
## Configuration
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `DYN_LORA_ENABLED` | Enable LoRA adapter support | `false` |
| `DYN_LORA_PATH` | Local cache directory for downloaded LoRAs | `~/.cache/dynamo_loras` |
| `AWS_ACCESS_KEY_ID` | S3 access key (for `s3://` URIs) | - |
| `AWS_SECRET_ACCESS_KEY` | S3 secret key (for `s3://` URIs) | - |
| `AWS_ENDPOINT` | Custom S3 endpoint (for MinIO, etc.) | - |
| `AWS_REGION` | AWS region | `us-east-1` |
| `AWS_ALLOW_HTTP` | Allow HTTP (non-TLS) connections | `false` |
### vLLM Arguments
| Argument | Description |
|----------|-------------|
| `--enable-lora` | Enable LoRA adapter support in vLLM |
| `--max-lora-rank` | Maximum LoRA rank (must be >= your LoRA's rank) |
| `--max-loras` | Maximum number of LoRAs to load simultaneously |
## Backend API Reference
### Load LoRA
Load a LoRA adapter from a source URI.
```text
POST /v1/loras
```
**Request:**
```json
{
"lora_name": "string",
"source": {
"uri": "string"
}
}
```
**Response:**
```json
{
"status": "success",
"message": "LoRA adapter 'my-lora' loaded successfully",
"lora_name": "my-lora",
"lora_id": 1207343256
}
```
### List LoRAs
List all loaded LoRA adapters.
```text
GET /v1/loras
```
**Response:**
```json
{
"status": "success",
"loras": {
"my-lora": 1207343256,
"another-lora": 987654321
},
"count": 2
}
```
### Unload LoRA
Unload a LoRA adapter from the worker.
```text
DELETE /v1/loras/{lora_name}
```
**Response:**
```json
{
"status": "success",
"message": "LoRA adapter 'my-lora' unloaded successfully",
"lora_name": "my-lora",
"lora_id": 1207343256
}
```
## Kubernetes Deployment
For Kubernetes deployments, use the `DynamoModel` Custom Resource to declaratively manage LoRA adapters.
### DynamoModel CRD
```yaml
apiVersion: nvidia.com/v1alpha1
kind: DynamoModel
metadata:
name: customer-support-lora
namespace: dynamo-system
spec:
modelName: customer-support-adapter-v1
baseModelName: Qwen/Qwen3-0.6B # Must match modelRef.name in DGD
modelType: lora
source:
uri: s3://my-models-bucket/loras/customer-support/v1
```
### How It Works
When you create a `DynamoModel`:
1. **Discovers endpoints**: Finds all pods running your `baseModelName`
2. **Creates service**: Automatically creates a Kubernetes Service
3. **Loads LoRA**: Calls the LoRA load API on each endpoint
4. **Updates status**: Reports which endpoints are ready
### Verify Deployment
```bash
# Check LoRA status
kubectl get dynamomodel customer-support-lora
# Expected output:
# NAME TOTAL READY AGE
# customer-support-lora 2 2 30s
```
For complete Kubernetes deployment details, see:
- [Managing Models with DynamoModel](../../kubernetes/deployment/dynamomodel-guide.md)
- [Kubernetes LoRA Deployment Example](../../../examples/backends/vllm/deploy/lora/README.md)
## Examples
| Example | Description |
|---------|-------------|
| [Local LoRA with MinIO](../../../examples/backends/vllm/launch/lora/README.md) | Local development with S3-compatible storage |
| [Kubernetes LoRA Deployment](../../../examples/backends/vllm/deploy/lora/README.md) | Production deployment with DynamoModel CRD |
## Troubleshooting
### LoRA Fails to Load
**Check S3 connectivity:**
```bash
# Verify LoRA exists in S3
aws --endpoint-url=$AWS_ENDPOINT s3 ls s3://my-loras/ --recursive
```
**Check cache directory:**
```bash
ls -la ~/.cache/dynamo_loras/
```
**Check worker logs:**
```bash
# Look for LoRA-related messages
kubectl logs deployment/my-worker | grep -i lora
```
### Model Not Found After Loading
- Verify the LoRA name matches exactly (case-sensitive)
- Check if the LoRA is listed: `curl http://localhost:8081/v1/loras`
- Ensure discovery registration succeeded (check worker logs)
### Inference Returns Base Model Response
- Verify the `model` field in your request matches the `lora_name`
- Check that the LoRA is loaded on the worker handling your request
- For disaggregated serving, ensure both prefill and decode workers have the LoRA
## See Also
- [Feature Matrix](../../reference/feature-matrix.md) - Backend compatibility overview
- [vLLM Backend](../../backends/vllm/README.md) - vLLM-specific configuration
- [Dynamo Operator](../../kubernetes/dynamo_operator.md) - Kubernetes operator overview
- [KV-Aware Routing](../../components/router/router_guide.md) - LoRA-aware request routing
<!--
SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
# Multimodal Inference in Dynamo
Dynamo supports multimodal inference across multiple LLM backends, enabling models to process images, video, and audio alongside text. This section provides comprehensive documentation for deploying multimodal models.
> [!IMPORTANT]
> **Security Requirement**: Multimodal processing must be explicitly enabled at startup.
> See the relevant documentation for each backend for the necessary flags.
>
> This prevents unintended processing of multimodal data from untrusted sources.
## Backend Documentation
```{toctree}
:maxdepth: 1
vLLM Multimodal <multimodal_vllm.md>
TensorRT-LLM Multimodal <multimodal_trtllm.md>
SGLang Multimodal <multimodal_sglang.md>
```
## Support Matrix
### Backend Capabilities
| Stack | E/PD | E/P/D | EP/D | EPD | Image | Video | Audio |
|-------|------|-------|------|-----|-------|-------|-------|
| **[vLLM](multimodal_vllm.md)** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 🧪 |
| **[TRT-LLM](multimodal_trtllm.md)** | ❌ | 🚧* | ✅ | ✅ | ✅ | ❌ | ❌ |
| **[SGLang](multimodal_sglang.md)** | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ |
\* E/P/D supported in TRT-LLM with pre-computed embeddings only; image URL support is WIP ([PR #4668](https://github.com/ai-dynamo/dynamo/pull/4668))
**Pattern Key:**
- **EPD** - All-in-one worker (Simple Aggregated)
- **E/PD** - Separate encode, combined prefill+decode
- **E/P/D** - All stages separate
- **EP/D** - Combined encode+prefill, separate decode
**Status:** ✅ Supported | 🚧 WIP | 🧪 Experimental | ❌ Not supported
### Input Format Support
| Format | vLLM | TRT-LLM | SGLang |
|--------|------|---------|--------|
| HTTP/HTTPS URL | ✅ | ✅ | ✅ |
| Data URL (Base64) | ✅ | ❌ | ❌ |
| Pre-computed Embeddings (.pt) | ❌ | ✅ | ❌ |
## Architecture Patterns
Dynamo supports several deployment patterns for multimodal inference based on two dimensions:
1. **Encoding**: Is media encoding handled inline (within prefill) or by a separate **Encode Worker**?
- *Inline*: Simpler setup, encoding happens in the prefill worker
- *Separate (EPD)*: Dedicated encode worker transfers embeddings via **NIXL (RDMA)**, enabling independent scaling
2. **Prefill/Decode**: Are prefill and decode in the same worker or separate?
- *Aggregated*: Single worker handles both prefill and decode
- *Disaggregated*: Separate workers for prefill and decode, with KV cache transfer between them
These combine into four deployment patterns:
### EPD - Simple Aggregated
All processing happens within a single worker - the simplest setup.
```text
HTTP Frontend (Rust)
Worker (Python)
↓ image load + encode + prefill + decode
Response
```
| Component | Purpose |
|-----------|---------|
| Frontend (Rust) | HTTP entry point, tokenization, image URL preprocessing |
| Worker | Complete inference pipeline (encode + prefill + decode) |
**When to use:** Quick setup, smaller models, development/testing.
### E/PD - Encode Separate
Encoding happens in a separate worker; prefill and decode share the same engine.
```text
HTTP Frontend (Rust)
Processor (Python)
↓ tokenizes, extracts media URL
Encode Worker (Python)
↓ downloads media, generates embeddings, NIXL transfer
PD Worker (Python)
↓ receives embeddings via NIXL, prefill + decode
Response
```
| Component | Purpose |
|-----------|---------|
| Frontend (Rust) | HTTP entry point |
| Processor (Python) | Tokenization, extracts media URLs |
| Encode Worker | Media encoding, embeddings generation |
| PD Worker | Prefill + Decode with embeddings |
**When to use:** Offload vision encoding to separate GPU, scale encode workers independently.
### E/P/D - Full Disaggregation
Full disaggregation with separate workers for encoding, prefill, and decode.
There are two variants of this workflow:
- Prefill-first, used by vLLM
- Decode-first, used by SGLang
Prefill-first:
```text
HTTP Frontend (Rust)
Processor (Python)
↓ tokenizes, extracts media URL
Encode Worker (Python)
↓ downloads media, generates embeddings, NIXL transfer
Prefill Worker (Python)
↓ receives embeddings via NIXL, prefill only, KV cache transfer
Decode Worker (Python)
↓ decode only, token generation
Response
```
OR
Decode-first:
```text
HTTP Frontend (Rust)
Processor (Python)
↓ tokenizes, extracts media URL
Encode Worker (Python)
↓ downloads media, generates embeddings, NIXL transfer
Decode Worker (Python)
↓ Bootstraps prefill worker
Prefill Worker (Python)
↓ receives embeddings via NIXL, prefill only, KV cache transfer
Decode Worker (Python)
↓ decode only, token generation
Response
```
| Component | Purpose |
|-----------|---------|
| Frontend (Rust) | HTTP entry point |
| Processor (Python) | Tokenization, extracts media URLs |
| Encode Worker | Media encoding, embeddings generation |
| Prefill Worker | Prefill only, transfers KV cache |
| Decode Worker | Decode only, token generation |
**When to use:** Maximum optimization, multi-node deployment, independent scaling of each phase.
### EP/D - Traditional Disaggregated
Encoding is combined with prefill, with decode separate.
```text
HTTP Frontend (Rust)
Processor (Python)
↓ tokenizes, extracts media URL
Encode+Prefill Worker (Python)
↓ downloads media, encodes inline, prefill, KV cache transfer
Decode Worker (Python)
↓ decode only, token generation
Response
```
| Component | Purpose |
|-----------|---------|
| Frontend (Rust) | HTTP entry point |
| Processor (Python) | Tokenization, extracts media URLs (vLLM only) |
| Encode+Prefill Worker | Combined encoding and prefill |
| Decode Worker | Decode only, token generation |
> **Note:** TRT-LLM's EP/D mode skips the Python Processor - the Rust frontend handles tokenization and routes directly to the Prefill worker.
> For multimodal requests, the Python prefill worker still re-tokenizes/builds inputs; Rust token_ids are ignored.
**When to use:** Models without pre-computed embedding support (Llama 4), or TRT-LLM disaggregated deployment.
## Example Workflows
You can find example workflows and reference implementations for deploying multimodal models in:
- [vLLM multimodal examples](https://github.com/ai-dynamo/dynamo/tree/main/examples/backends/vllm/launch)
- [TRT-LLM multimodal examples](https://github.com/ai-dynamo/dynamo/tree/main/examples/backends/trtllm/launch)
- [SGLang multimodal examples](https://github.com/ai-dynamo/dynamo/tree/main/examples/backends/sglang/launch)
- [Advanced multimodal examples](https://github.com/ai-dynamo/dynamo/tree/main/examples/multimodal/launch) (video, audio)
<!--
SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
# SGLang Multimodal
This document provides a comprehensive guide for multimodal inference using SGLang backend in Dynamo. SGLang multimodal supports **EPD**, **E/PD**, and **E/P/D** flows, with NIXL (RDMA) for zero-copy tensor transfer in disaggregated modes.
## Support Matrix
| Modality | Input Format | Aggregated | Disaggregated | Notes |
|----------|--------------|------------|---------------|-------|
| **Image** | HTTP/HTTPS URL | Yes | Yes | Vision encoder generates embeddings |
| **Image** | Data URL (Base64) | No | No | |
| **Video** | HTTP/HTTPS URL | No | No | |
| **Audio** | HTTP/HTTPS URL | No | No | |
### Supported URL Formats
| Format | Example | Description |
|--------|---------|-------------|
| **HTTP/HTTPS** | `http://example.com/image.jpg` | Remote media files |
## Deployment Patterns
SGLang supports EPD, E/PD, and E/P/D patterns. See [Multimodal Architecture Patterns](README.md#architecture-patterns) for detailed explanations.
| Pattern | Supported | Launch Script | Notes |
|---------|-----------|---------------|-------|
| EPD (Simple Aggregated) | ✅ | `agg.sh` | Internal encoding |
| E/PD (Encode Separate) | ✅ | `multimodal_epd.sh` | Vision encoder separate |
| E/P/D (Full Disaggregation) | ✅ | `multimodal_disagg.sh` | KV cache via bootstrap |
| EP/D (Traditional Disaggregated) | ❌ | N/A | Not supported |
### Component Flags
| Component | Flag | Purpose |
|-----------|------|---------|
| Processor | `--multimodal-processor` | HTTP entry, OpenAI→SGLang conversion |
| Encode Worker | `--multimodal-encode-worker` | Vision encoder, embeddings generation |
| PD Worker | `--multimodal-worker` | Prefill + Decode with embeddings |
| Decode Worker | `--multimodal-worker --serving-mode=decode` | Entry point for disaggregation |
| Prefill Worker | `--multimodal-worker --serving-mode=prefill` | Called by Decode, bootstrap coordination |
### SGLang-Specific Characteristics
- **Vision Encoder in Python**: Encode worker loads vision model (AutoModel) and image processor (AutoImageProcessor)
- **Token Expansion**: Single `<|image_pad|>` token replaced with N tokens based on embedding shape
- **NIXL Transfer**: Embeddings transferred from Encoder → PD Worker using NIXL
- **No Rust Processing**: All tokenization and image handling happens in Python
## Use the Latest Release
We recommend using the latest stable release of dynamo to avoid breaking changes:
[![GitHub Release](https://img.shields.io/github/v/release/ai-dynamo/dynamo)](https://github.com/ai-dynamo/dynamo/releases/latest)
You can find the [latest release](https://github.com/ai-dynamo/dynamo/releases/latest) and check out the corresponding branch with:
```bash
git checkout $(git describe --tags $(git rev-list --tags --max-count=1))
```
## EPD Serving (Simple Aggregated)
### Components
- worker: [DecodeWorkerHandler](../../components/src/dynamo/sglang/request_handlers/llm/decode_handler.py) handles encoding, prefilling, and decoding in a single process.
### Workflow
The `DecodeWorkerHandler` receives multimodal requests with image URLs and passes them directly to SGLang's engine. SGLang's internal `mm_data_processor` handles image fetching, loading, encoding, and token expansion.
```mermaid
flowchart LR
HTTP --> worker
worker --tokenized text + image_urls--> SGLang[SGLang Engine]
```
### Launch
```bash
cd $DYNAMO_HOME/examples/backends/sglang
./launch/agg.sh --model Qwen/Qwen2.5-VL-7B-Instruct --chat-template qwen2-vl
```
**Client:**
```bash
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen2.5-VL-7B-Instruct",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Describe the image."
},
{
"type": "image_url",
"image_url": {
"url": "http://images.cocodataset.org/test2017/000000155781.jpg"
}
}
]
}
],
"max_tokens": 50,
"stream": false
}' | jq
```
## E/PD Serving (Encode Separate)
### Components
- workers:
- [MultimodalEncodeWorkerHandler](../../components/src/dynamo/sglang/request_handlers/multimodal/encode_worker_handler.py) for encoding
- [MultimodalWorkerHandler](../../components/src/dynamo/sglang/request_handlers/multimodal/worker_handler.py) for prefilling and decoding.
- processor: [MultimodalProcessorHandler](../../components/src/dynamo/sglang/request_handlers/multimodal/processor_handler.py)
- tokenizes the prompt using the chat template
- passes the text and image url to the MultimodalEncodeWorker.
### Workflow
The `MultimodalEncodeWorker` downloads and encodes the image and passes the embeddings to the MultimodalWorker. The work complete event is sent via NATS, while the embeddings tensor is transferred via RDMA through the NIXL interface. The `MultimodalWorker` then prefills and decodes the prompt in the same engine, as in the [LLM aggregated serving](../../backends/sglang/README.md) example. Only the processor is registered to the Dynamo frontend as an available endpoint. Workers do NOT register - they are internal components and communicate via NATS.
```mermaid
flowchart LR
HTTP --> processor
processor --tokenized request + image_url--> encode_worker
encode_worker --request + embeddings--> worker
worker -.-> encode_worker
encode_worker -.-> processor
processor -.-> HTTP
```
### Launch
```bash
cd $DYNAMO_HOME/examples/backends/sglang
./launch/multimodal_epd.sh
```
**Client:**
```bash
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen2.5-VL-7B-Instruct",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Describe the image."
},
{
"type": "image_url",
"image_url": {
"url": "http://images.cocodataset.org/test2017/000000155781.jpg"
}
}
]
}
],
"max_tokens": 50,
"stream": false
}' | jq
```
## E/P/D Serving (Full Disaggregation)
### Components
- workers:
- [MultimodalEncodeWorkerHandler](../../components/src/dynamo/sglang/request_handlers/multimodal/encode_worker_handler.py) for encoding
- [MultimodalWorkerHandler](../../components/src/dynamo/sglang/request_handlers/multimodal/worker_handler.py) for decoding
- [MultimodalPrefillWorkerHandler](../../components/src/dynamo/sglang/request_handlers/multimodal/worker_handler.py) for prefilling
- processor: [MultimodalProcessorHandler](../../components/src/dynamo/sglang/request_handlers/multimodal/processor_handler.py) tokenizes the prompt and passes it to the MultimodalEncodeWorker.
### Workflow
In models like Qwen2.5-VL, embeddings are only required during the prefill stage. The image embeddings are transferred via NIXL from the Encode Worker to the Decode Worker (the entry point for disaggregation), which then coordinates with the Prefill Worker. The Prefill Worker processes the embeddings and forwards the KV cache back to the Decode Worker for token generation.
```mermaid
flowchart LR
HTTP --> processor
processor --tokenized request + image_url--> encode_worker
encode_worker --request + embeddings--> worker
worker --request + embeddings--> prefill_worker
prefill_worker --KV Cache--> worker
encode_worker -.-> processor
worker -.-> encode_worker
processor -.-> HTTP
```
### Launch
```bash
cd $DYNAMO_HOME/examples/backends/sglang
./launch/multimodal_disagg.sh
```
**Client:**
```bash
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen2.5-VL-7B-Instruct",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Describe the image."
},
{
"type": "image_url",
"image_url": {
"url": "http://images.cocodataset.org/test2017/000000155781.jpg"
}
}
]
}
],
"max_tokens": 50,
"stream": false
}' | jq
```
## Bootstrap Coordination
SGLang disaggregation uses a bootstrap mechanism for P->D coordination:
### Request Flow (Important)
```text
Client → Frontend → Processor → Encode → DECODE Worker → Prefill Worker
Entry point for disaggregation!
```
### Bootstrap Process
1. **Decode Worker** receives request from Encode Worker
2. **Decode Worker** calls Prefill Worker via NATS to request bootstrap info
3. **Prefill Worker** generates `{host, port, room}` and returns immediately
4. **Both workers** connect to same "room" using bootstrap coordinates
5. **SGLang internally** transfers KV cache state via bootstrap connection (not NIXL)
### Key Difference from vLLM
- vLLM: Frontend → Prefill → Decode (Prefill is entry point)
- SGLang: Frontend → Processor → Encode → **Decode → Prefill** (Decode is entry point)
## Inter-Component Communication
### Control Flow (NATS)
All component-to-component communication happens via NATS:
#### E/PD Mode (Encode Separate)
```text
Processor → Encode Worker → PD Worker
(NATS) (NATS + NIXL embeddings)
```
#### E/P/D Mode (Full Disaggregation)
```text
Processor → Encode Worker → DECODE Worker → Prefill Worker
(NATS) (NATS) (NATS)
Decode requests bootstrap
Prefill returns {host, port, room}
Both connect via bootstrap
SGLang internal KV cache transfer
```
### Detailed Message Flow
```text
Processor → Encode Worker:
- NATS round_robin with SglangMultimodalRequest
- Contains: tokenized input_ids, image URL, sampling params
Encode Worker → Decode/PD Worker:
- NATS round_robin to "backend" component
- Contains: expanded token_ids, NIXL metadata, embeddings shape
- NIXL transfer: embeddings tensor
Decode Worker → Prefill Worker (disagg only):
- NATS call to "prefill" component
- Decode requests bootstrap coordinates
- Prefill returns: {bootstrap_host, bootstrap_port, bootstrap_room}
Prefill ↔ Decode (via bootstrap):
- SGLang internal connection (not NATS)
- KV cache state shared via bootstrap mechanism
```
### Data Transfer (NIXL)
NIXL is used only for embedding transfer:
```python
# Encode Worker
descriptor = connect.Descriptor(precomputed_embeddings)
with connector.create_readable(descriptor) as readable:
request.serialized_request = readable.metadata()
await pd_worker_client.round_robin(request)
await readable.wait_for_completion()
# PD Worker
embeddings = torch.empty(request.embeddings_shape, dtype=torch.float16)
descriptor = connect.Descriptor(embeddings)
read_op = await connector.begin_read(request.serialized_request, descriptor)
await read_op.wait_for_completion()
```
## Vision Encoding Details
### Encode Worker Components
The encode worker loads and runs the vision model in Python:
```python
self.image_processor = AutoImageProcessor.from_pretrained(
model_path, trust_remote_code=True
)
self.vision_model = AutoModel.from_pretrained(
model_path,
device_map="auto",
torch_dtype=torch.float16,
trust_remote_code=True
)
```
### Token Expansion Process
1. Processor inserts single image token (e.g., `<|image_pad|>`)
2. Encode worker generates embeddings: `shape = (batch, num_patches, hidden_dim)`
3. Encode worker replaces single token with `num_patches` tokens
4. Downstream worker receives expanded token sequence
Example:
```python
# Before: ["Hello", "<|image_pad|>", "world"]
# After: ["Hello", "<|image_pad|>", "<|image_pad|>", ...(576 tokens), "world"]
```
## Chat Template Processing
SGLang uses its own chat template system:
```python
from sglang.srt.parser.conversation import chat_templates
conv = chat_templates["qwen2-vl"].copy()
conv.append_message(conv.roles[0], f"{conv.image_token} Describe this image")
processed = tokenizer(text=conv.get_prompt(), return_tensors="pt")
```
Supported templates: `qwen2-vl`, `llama-3`, `vicuna`, etc.
## NIXL Usage
| Use Case | NIXL Used? | Data Transfer | Notes |
|----------|------------|---------------|-------|
| EPD (Simple Aggregated) | No | N/A | All processing internal to SGLang |
| E/PD (Encode Separate) | Yes | Encoder → PD (embeddings) | Vision encoder separate |
| E/P/D (Full Disaggregation) | Yes | Encoder → Prefill (embeddings) | KV cache via SGLang bootstrap |
**Key Difference:** SGLang P/D uses bootstrap mechanism, not NIXL for KV cache like vLLM.
## Known Limitations
- **No Data URL support** - Only HTTP/HTTPS URLs supported; `data:image/...` base64 URLs not supported
- **No pre-computed embeddings** - Cannot use `.pt`, `.pth`, `.bin` embedding files; vision encoder runs for every request
- **No video support** - No video encoder implementation
- **No audio support** - No audio encoder implementation
- **Only Processor registers with Dynamo** - Workers are internal components, frontend routes to Processor only
- **Disaggregated routing** - Decode Worker is the entry point (calls Prefill), cannot route directly to Prefill workers
- **Limited model generalization** - Token expansion logic is model-specific; adding new models may require implementation updates
## Supported Models
SGLang multimodal **only supports image-based vision-language models**:
- **Qwen2-VL** / **Qwen2.5-VL** (primary support)
- Models with `AutoImageProcessor` and vision tower
- Models compatible with SGLang's image embedding format
## Key Files
| File | Description |
|------|-------------|
| `components/src/dynamo/sglang/main.py` | Component initialization, only Processor registers |
| `components/src/dynamo/sglang/request_handlers/multimodal/processor_handler.py` | Processor implementation, OpenAI→SGLang |
| `components/src/dynamo/sglang/request_handlers/multimodal/encode_worker_handler.py` | Vision encoder, embeddings generation |
| `components/src/dynamo/sglang/request_handlers/multimodal/worker_handler.py` | PD/Prefill/Decode workers, NIXL read |
| `components/src/dynamo/sglang/multimodal_utils/multimodal_chat_processor.py` | Chat template processing |
| `components/src/dynamo/sglang/protocol.py` | Request/response data structures |
| `components/src/dynamo/sglang/register.py` | Registration logic (only called for Processor) |
<!--
SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
# TensorRT-LLM Multimodal
This document provides a comprehensive guide for multimodal inference using TensorRT-LLM backend in Dynamo.
You can provide multimodal inputs in the following ways:
- By sending image URLs
- By providing paths to pre-computed embedding files
> **Note:** You should provide **either image URLs or embedding file paths** in a single request.
## Support Matrix
| Modality | Input Format | Aggregated | Disaggregated | Notes |
|----------|--------------|------------|---------------|-------|
| **Image** | HTTP/HTTPS URL | Yes | Yes | Full support for all image models |
| **Image** | Pre-computed Embeddings (.pt, .pth, .bin) | Yes | Yes | Direct embedding files |
| **Video** | HTTP/HTTPS URL | No | No | Not implemented |
| **Audio** | HTTP/HTTPS URL | No | No | Not implemented |
### Supported URL Formats
| Format | Example | Description |
|--------|---------|-------------|
| **HTTP/HTTPS** | `http://example.com/image.jpg` | Remote media files |
| **Pre-computed Embeddings** | `/path/to/embedding.pt` | Local embedding files (.pt, .pth, .bin) |
## Deployment Patterns
TRT-LLM supports aggregated and traditional disaggregated patterns. See [Architecture Patterns](README.md#architecture-patterns) for detailed explanations.
| Pattern | Supported | Launch Script | Notes |
|---------|-----------|---------------|-------|
| Aggregated | ✅ | `agg.sh` | Easiest setup, single worker |
| EP/D (Traditional Disaggregated) | ✅ | `disagg_multimodal.sh` | Prefill handles encoding, 2 workers |
| E/P/D (Full - Image URLs) | ✅ | `epd_multimodal_image_and_embeddings.sh` | Standalone encoder with `MultimodalEncoder`, 3 workers |
| E/P/D (Full - Pre-computed Embeddings) | ✅ | `epd_multimodal_image_and_embeddings.sh` | Standalone encoder with NIXL transfer, 3 workers |
| E/P/D (Large Models) | ✅ | `epd_disagg.sh` | For Llama-4 Scout/Maverick, multi-node |
### Component Flags
| Component | Flag | Purpose |
|-----------|------|---------|
| Worker | `--modality multimodal` | Complete pipeline (aggregated) |
| Prefill Worker | `--disaggregation-mode prefill` | Image processing + Prefill (multimodal tokenization happens here) |
| Decode Worker | `--disaggregation-mode decode` | Decode only |
| Encode Worker | `--disaggregation-mode encode` | Image encoding (E/P/D flow) |
## Aggregated Serving
Quick steps to launch Llama-4 Maverick BF16 in aggregated mode:
```bash
cd $DYNAMO_HOME
export AGG_ENGINE_ARGS=./examples/backends/trtllm/engine_configs/llama4/multimodal/agg.yaml
export SERVED_MODEL_NAME="meta-llama/Llama-4-Maverick-17B-128E-Instruct"
export MODEL_PATH="meta-llama/Llama-4-Maverick-17B-128E-Instruct"
./examples/backends/trtllm/launch/agg.sh
```
**Client:**
```bash
curl localhost:8000/v1/chat/completions -H "Content-Type: application/json" -d '{
"model": "meta-llama/Llama-4-Maverick-17B-128E-Instruct",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Describe the image"
},
{
"type": "image_url",
"image_url": {
"url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/inpaint.png"
}
}
]
}
],
"stream": false,
"max_tokens": 160
}'
```
## Disaggregated Serving
Example using `Qwen/Qwen2-VL-7B-Instruct`:
```bash
cd $DYNAMO_HOME
export MODEL_PATH="Qwen/Qwen2-VL-7B-Instruct"
export SERVED_MODEL_NAME="Qwen/Qwen2-VL-7B-Instruct"
export PREFILL_ENGINE_ARGS="examples/backends/trtllm/engine_configs/qwen2-vl-7b-instruct/prefill.yaml"
export DECODE_ENGINE_ARGS="examples/backends/trtllm/engine_configs/qwen2-vl-7b-instruct/decode.yaml"
export MODALITY="multimodal"
./examples/backends/trtllm/launch/disagg.sh
```
```bash
curl localhost:8000/v1/chat/completions -H "Content-Type: application/json" -d '{
"model": "Qwen/Qwen2-VL-7B-Instruct",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Describe the image"
},
{
"type": "image_url",
"image_url": {
"url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/inpaint.png"
}
}
]
}
],
"stream": false,
"max_tokens": 160
}'
```
For a large model like `meta-llama/Llama-4-Maverick-17B-128E-Instruct`, a multi-node setup is required for disaggregated serving (see [Multi-node Deployment](#multi-node-deployment-slurm) below), while aggregated serving can run on a single node. This is because the model with a disaggregated configuration is too large to fit on a single node's GPUs. For instance, running this model in disaggregated mode requires 2 nodes with 8xH200 GPUs or 4 nodes with 4xGB200 GPUs.
## Full E/P/D Flow (Image URLs)
For high-performance multimodal inference, Dynamo supports a standalone encoder with an **Encode-Prefill-Decode (E/P/D)** flow using TRT-LLM's `MultimodalEncoder`. This separates the vision encoding stage from prefill and decode, enabling better GPU utilization and scalability.
### Supported Input Formats
| Format | Example | Description |
|--------|---------|-------------|
| **HTTP/HTTPS URL** | `https://example.com/image.jpg` | Remote image files |
| **Base64 Data URL** | `data:image/jpeg;base64,...` | Inline base64-encoded images |
### How It Works
In the full E/P/D flow:
1. **Encode Worker**: Runs TRT-LLM's `MultimodalEncoder.generate()` to process image URLs through the vision encoder and projector
2. **Prefill Worker**: Receives `disaggregated_params` containing multimodal embedding handles, processes context and generates KV cache
3. **Decode Worker**: Performs streaming token generation using the KV cache
The encode worker uses TRT-LLM's `MultimodalEncoder` class (which inherits from `BaseLLM`) and only requires the model path and batch size - no KV cache configuration is needed since it only runs the vision encoder + projector.
### How to Launch
```bash
cd $DYNAMO_HOME
# Launch 3-worker E/P/D flow with image URL support
./examples/backends/trtllm/launch/epd_multimodal_image_and_embeddings.sh
```
### Example Request
```bash
curl localhost:8000/v1/chat/completions -H "Content-Type: application/json" -d '{
"model": "llava-v1.6-mistral-7b-hf",
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "Describe the image"},
{
"type": "image_url",
"image_url": {
"url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/inpaint.png"
}
}
]
}
],
"max_tokens": 160
}'
```
### E/P/D Architecture (Image URLs)
```mermaid
sequenceDiagram
participant Client
participant Frontend
participant PrefillWorker as "Prefill Worker"
participant EncodeWorker as "Encode Worker"
participant DecodeWorker as "Decode Worker"
Client->>Frontend: POST /v1/chat/completions (image URL)
Frontend->>PrefillWorker: Route to prefill worker
PrefillWorker->>EncodeWorker: Send request (image URL)
Note over EncodeWorker: MultimodalEncoder.generate()<br/>runs vision encoder + projector
EncodeWorker->>PrefillWorker: Return disaggregated_params<br/>(multimodal_embedding_handles)
Note over PrefillWorker: Process context with embeddings<br/>Generate KV cache
PrefillWorker->>Frontend: Return prefill response
Frontend->>DecodeWorker: Route to decode worker
DecodeWorker->>Frontend: Stream response chunks
Frontend->>Client: Stream response
```
### Key Differences from EP/D (Traditional Disaggregated)
| Aspect | EP/D (Traditional) | E/P/D (Full) |
|--------|-------------------|--------------|
| **Encoding** | Prefill worker handles image encoding | Dedicated encode worker |
| **Prefill Load** | Higher (encoding + prefill) | Lower (prefill only) |
| **Use Case** | Simpler setup | Better scalability for vision-heavy workloads |
| **Launch Script** | `disagg_multimodal.sh` | `epd_multimodal_image_and_embeddings.sh` |
## Pre-computed Embeddings with E/P/D Flow
For high-performance multimodal inference, Dynamo supports pre-computed embeddings with an **Encode-Prefill-Decode (E/P/D)** flow using **NIXL (RDMA)** for zero-copy tensor transfer.
### Supported File Types
- `.pt` - PyTorch tensor files
- `.pth` - PyTorch checkpoint files
- `.bin` - Binary tensor files
### Embedding File Formats
TRT-LLM supports two formats for embedding files:
**1. Simple Tensor Format**
Direct tensor saved as `.pt` file containing only the embedding tensor:
```python
embedding_tensor = torch.rand(1, 576, 4096) # [batch, seq_len, hidden_dim]
torch.save(embedding_tensor, "embedding.pt")
```
**2. Dictionary Format with Auxiliary Data**
Dictionary containing multiple keys, used by models like Llama-4 that require additional metadata:
```python
embedding_dict = {
"mm_embeddings": torch.rand(1, 576, 4096),
"special_tokens": [128256, 128257],
"image_token_offsets": [[0, 576]],
# ... other model-specific metadata
}
torch.save(embedding_dict, "llama4_embedding.pt")
```
- **Simple tensors**: Loaded directly and passed to `mm_embeddings` parameter
- **Dictionary format**: `mm_embeddings` key extracted as main tensor, other keys preserved as auxiliary data
### How to Launch
```bash
cd $DYNAMO_HOME/examples/backends/trtllm
# Launch 3-worker E/P/D flow with NIXL
./launch/epd_disagg.sh
```
> **Note:** This script is designed for 8-node H200 with `Llama-4-Scout-17B-16E-Instruct` model and assumes you have a model-specific embedding file ready.
### Configuration
```bash
# Encode endpoint for Prefill → Encode communication
export ENCODE_ENDPOINT="dyn://dynamo.tensorrt_llm_encode.generate"
# Security: Allowed directory for embedding files (default: /tmp)
export ALLOWED_LOCAL_MEDIA_PATH="/tmp"
# Security: Max file size to prevent DoS attacks (default: 50MB)
export MAX_FILE_SIZE_MB=50
```
### Example Request with Pre-computed Embeddings
```bash
curl localhost:8000/v1/chat/completions -H "Content-Type: application/json" -d '{
"model": "meta-llama/Llama-4-Maverick-17B-128E-Instruct",
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "Describe the image"},
{"type": "image_url", "image_url": {"url": "/path/to/embedding.pt"}}
]
}
],
"max_tokens": 160
}'
```
### E/P/D Architecture
The E/P/D flow implements a **3-worker architecture**:
- **Encode Worker**: Loads pre-computed embeddings, transfers via NIXL
- **Prefill Worker**: Receives embeddings, handles context processing and KV-cache generation
- **Decode Worker**: Performs streaming token generation
```mermaid
sequenceDiagram
participant Client
participant Frontend
participant PrefillWorker as "Prefill Worker"
participant EncodeWorker as "Encode Worker"
participant DecodeWorker as "Decode Worker"
participant NIXL as "NIXL (RDMA)"
Client->>Frontend: POST /v1/chat/completions
Frontend->>PrefillWorker: Route to prefill worker
PrefillWorker->>EncodeWorker: Send request (embedding paths)
EncodeWorker->>NIXL: Create readable operation
EncodeWorker->>PrefillWorker: Send metadata + NIXL info
PrefillWorker->>NIXL: Begin read operation
NIXL-->>PrefillWorker: Zero-copy transfer complete
PrefillWorker->>Frontend: Return prefill response
Frontend->>DecodeWorker: Route to decode worker
DecodeWorker->>Frontend: Stream response chunks
Frontend->>Client: Stream response
```
## Multi-node Deployment (Slurm)
This section demonstrates how to deploy large multimodal models that require a multi-node setup using Slurm.
> **Note:** The scripts referenced in this section can be found in [`examples/basics/multinode/trtllm/`](https://github.com/ai-dynamo/dynamo/tree/main/examples/basics/multinode/trtllm/).
### Environment Setup
Assuming you have allocated your nodes via `salloc` and are inside an interactive shell:
```bash
# Container image (build using docs/backends/trtllm/README.md#build-container)
export IMAGE="<dynamo_trtllm_image>"
# Host:container path pairs for mounting
export MOUNTS="${PWD}/../../../../:/mnt"
# Model configuration
export MODEL_PATH="meta-llama/Llama-4-Maverick-17B-128E-Instruct"
export SERVED_MODEL_NAME="meta-llama/Llama-4-Maverick-17B-128E-Instruct"
export MODALITY=${MODALITY:-"multimodal"}
```
### Multi-node Disaggregated Launch
For 4 4xGB200 nodes (2 for prefill, 2 for decode):
```bash
# Customize parallelism to match your engine configs
# export PREFILL_ENGINE_CONFIG="/mnt/examples/backends/trtllm/engine_configs/llama4/multimodal/prefill.yaml"
# export DECODE_ENGINE_CONFIG="/mnt/examples/backends/trtllm/engine_configs/llama4/multimodal/decode.yaml"
# export NUM_PREFILL_NODES=2
# export NUM_DECODE_NODES=2
# export NUM_GPUS_PER_NODE=4
# Launches frontend + etcd/nats on head node, plus prefill and decode workers
./srun_disaggregated.sh
```
### Understanding the Output
1. `srun_disaggregated.sh` launches three srun jobs: frontend, prefill worker, and decode worker
2. The OpenAI frontend will dynamically discover workers as they register:
```text
INFO dynamo_run::input::http: Watching for remote model at models
INFO dynamo_llm::http::service::service_v2: Starting HTTP service on: 0.0.0.0:8000
```
3. TRT-LLM workers output progress from each MPI rank while loading
4. When ready, the frontend logs:
```text
INFO dynamo_llm::discovery::watcher: added model model_name="meta-llama/Llama-4-Maverick-17B-128E-Instruct"
```
### Cleanup
```bash
pkill srun
```
## NIXL Usage
| Use Case | Script | NIXL Used? | Data Transfer |
|----------|--------|------------|---------------|
| Aggregated | `agg.sh` | No | All in one worker |
| EP/D (Traditional Disaggregated) | `disagg_multimodal.sh` | Optional | Prefill → Decode (KV cache via UCX or NIXL) |
| E/P/D (Image URLs) | `epd_multimodal_image_and_embeddings.sh` | No | Encoder → Prefill (handles via params), Prefill → Decode (KV cache) |
| E/P/D (Pre-computed Embeddings) | `epd_multimodal_image_and_embeddings.sh` | Yes | Encoder → Prefill (embeddings via NIXL RDMA) |
| E/P/D (Large Models) | `epd_disagg.sh` | Yes | Encoder → Prefill (embeddings via NIXL), Prefill → Decode (KV cache) |
> **Note:** NIXL for KV cache transfer is currently beta and only supported on AMD64 (x86_64) architecture.
## ModelInput Types and Registration
TRT-LLM workers register with Dynamo using:
| ModelInput Type | Preprocessing | Use Case |
|-----------------|---------------|----------|
| `ModelInput.Tokens` | Rust frontend may tokenize, but multimodal flows re-tokenize and build inputs in the Python worker; Rust token_ids are ignored | All TRT-LLM workers |
```python
# TRT-LLM Worker - Register with Tokens
await register_llm(
ModelInput.Tokens, # Rust does minimal preprocessing
model_type, # ModelType.Chat or ModelType.Prefill
generate_endpoint,
model_name,
...
)
```
## Inter-Component Communication
| Transfer Stage | Message | NIXL Transfer |
|----------------|---------|---------------|
| **Frontend → Prefill** | Request with image URL or embedding path | No |
| **Prefill → Encode (Image URL)** | Request with image URL | No |
| **Encode → Prefill (Image URL)** | `ep_disaggregated_params` with `multimodal_embedding_handles`, processed prompt, and token IDs | No |
| **Prefill → Encode (Embedding Path)** | Request with embedding file path | No |
| **Encode → Prefill (Embedding Path)** | NIXL readable metadata + shape/dtype + auxiliary data | Yes (Embeddings tensor via RDMA) |
| **Prefill → Decode** | `disaggregated_params` with `_epd_metadata` (prompt, token IDs) | Configurable (KV cache: NIXL default, UCX optional) |
## Known Limitations
- **No video support** - No video encoder implementation
- **No audio support** - No audio encoder implementation
- **Multimodal preprocessing/tokenization happens in Python** - Rust may forward token_ids, but multimodal requests are parsed and re-tokenized in the Python worker
- **Multi-node H100 limitation** - Loading `meta-llama/Llama-4-Maverick-17B-128E-Instruct` with 8 nodes of H100 with TP=16 is not possible due to head count divisibility (`num_attention_heads: 40` not divisible by `tp_size: 16`)
- **llava-v1.6-mistral-7b-hf model crash** - Known issue with TRTLLM backend compatibility with `TensorRT LLM version: 1.2.0rc6.post1`. To use Llava model download revision `revision='52320fb52229` locally using HF.
- **Embeddings file crash** - Known issue with TRTLLM backend compatibility with `TensorRT LLM version: 1.2.0rc6.post1`. Embedding file parsing crashes in `attach_multimodal_embeddings(`. To be fixed in next TRTLLM upgrade.
## Supported Models
Multimodal models listed in [TensorRT-LLM supported models](https://github.com/NVIDIA/TensorRT-LLM/blob/main/docs/source/models/supported-models.md) are supported by Dynamo.
Common examples:
- **Llama 4 Vision models** (Maverick, Scout) - Recommended for large-scale deployments
- **LLaVA models** (e.g., `llava-hf/llava-v1.6-mistral-7b-hf`) - Default model for E/P/D examples
- **Qwen2-VL models** - Supported in traditional disaggregated mode
- Other vision-language models with TRT-LLM support
## Key Files
| File | Description |
|------|-------------|
| `components/src/dynamo/trtllm/main.py` | Worker initialization and setup |
| `components/src/dynamo/trtllm/engine.py` | TensorRTLLMEngine wrapper (LLM and MultimodalEncoder) |
| `components/src/dynamo/trtllm/constants.py` | DisaggregationMode enum (AGGREGATED, PREFILL, DECODE, ENCODE) |
| `components/src/dynamo/trtllm/encode_helper.py` | Encode worker request processing (embedding-path and full EPD flows) |
| `components/src/dynamo/trtllm/multimodal_processor.py` | Multimodal request processing |
| `components/src/dynamo/trtllm/request_handlers/handlers.py` | Request handlers (EncodeHandler, PrefillHandler, DecodeHandler) |
| `components/src/dynamo/trtllm/request_handlers/handler_base.py` | Base handler with disaggregated params encoding/decoding |
| `components/src/dynamo/trtllm/utils/disagg_utils.py` | DisaggregatedParamsCodec for network transfer |
| `components/src/dynamo/trtllm/utils/trtllm_utils.py` | Command-line argument parsing |
<!--
SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
# vLLM Multimodal
This document provides a comprehensive guide for multimodal inference using vLLM backend in Dynamo.
> [!IMPORTANT]
> **Security Requirement**: All multimodal workers require the `--enable-multimodal` flag to be explicitly set at startup. This is a security feature to prevent unintended processing of multimodal data from untrusted sources. Workers will fail at startup if multimodal flags (e.g., `--multimodal-worker`, `--multimodal-processor`) are used without `--enable-multimodal`.
> This flag is analogous to `--enable-mm-embeds` in vllm serve but also extends it to all multimodal content (url, embeddings, b64).
## Support Matrix
| Modality | Input Format | Aggregated | Disaggregated | Notes |
|----------|--------------|------------|---------------|-------|
| **Image** | HTTP/HTTPS URL | Yes | Yes | Full support for all image models |
| **Image** | Data URL (Base64) | Yes | Yes | Inline base64-encoded images |
| **Video** | HTTP/HTTPS URL | Yes | Yes | Frame extraction and processing |
| **Audio** | HTTP/HTTPS URL | Yes | Yes | Experimental - requires audio dependencies |
### Supported URL Formats
| Format | Example | Description |
|--------|---------|-------------|
| **HTTP/HTTPS** | `http://example.com/image.jpg` | Remote media files |
| **Data URL** | `data:image/jpeg;base64,/9j/4AAQ...` | Base64-encoded inline data |
## Deployment Patterns
vLLM supports all multimodal deployment patterns. See [Architecture Patterns](README.md#architecture-patterns) for detailed explanations.
| Pattern | Supported | Launch Script | Notes |
|---------|-----------|---------------|-------|
| EPD (Simple Aggregated) | ✅ | `agg_multimodal.sh` | Easiest setup |
| E/PD (Encode Separate) | ✅ | `agg_multimodal_epd.sh` | Separate encode worker |
| E/P/D (Full Disaggregation) | ✅ | `disagg_multimodal_epd.sh` | All stages separate |
| EP/D (Traditional Disaggregated) | ✅ | `disagg_multimodal_llama.sh` | For Llama 4 models |
| E/PD (EC Connector) | ✅ | `agg_multimodal_ec_connector.sh` | vLLM-native encoder with ECConnector |
### Component Flags
| Component | Flag | Purpose |
|-----------|------|---------|
| Processor | `--multimodal-processor` | HTTP entry, tokenization |
| Encode Worker | `--multimodal-encode-worker` | Media encoding |
| PD Worker | `--multimodal-worker` | Prefill + Decode |
| Prefill Worker | `--multimodal-worker --is-prefill-worker` | Prefill only |
| Decode Worker | `--multimodal-decode-worker` | Decode only |
| Encode+Prefill Worker | `--multimodal-encode-prefill-worker --is-prefill-worker` | Combined (Llama 4) |
| vLLM Native Encoder | `--vllm-native-encoder-worker` | vLLM-native encoding with ECConnector |
## Use the Latest Release
We recommend using the latest stable release of dynamo to avoid breaking changes:
[![GitHub Release](https://img.shields.io/github/v/release/ai-dynamo/dynamo)](https://github.com/ai-dynamo/dynamo/releases/latest)
You can find the [latest release](https://github.com/ai-dynamo/dynamo/releases/latest) and check out the corresponding branch with:
```bash
git checkout $(git describe --tags $(git rev-list --tags --max-count=1))
```
## Image Serving
### E/PD Serving (Encode Separate)
**Components:**
- workers: [EncodeWorkerHandler](../../components/src/dynamo/vllm/multimodal_handlers/encode_worker_handler.py) for encoding and [MultimodalPDWorkerHandler](../../components/src/dynamo/vllm/multimodal_handlers/worker_handler.py) for prefilling and decoding.
- processor: Tokenizes the prompt and passes it to the EncodeWorkerHandler.
- frontend: HTTP endpoint to handle incoming requests.
**Workflow:**
The EncodeWorkerHandler encodes the image and passes the embeddings to the MultimodalPDWorkerHandler via NATS and RDMA. The work complete event is sent via NATS, while the embeddings tensor is transferred via RDMA through the NIXL interface.
```mermaid
flowchart LR
HTTP --> processor
processor --> HTTP
processor --image_url--> encode_worker
encode_worker --> processor
encode_worker --embeddings--> pd_worker
pd_worker --> encode_worker
```
> **Note:** Aggregated serving supports LLaVA 1.5 7B and Qwen2.5-VL-7B-Instruct. Disaggregated serving is currently only confirmed for LLaVA.
**Launch:**
```bash
cd $DYNAMO_HOME/examples/backends/vllm
# Serve a LLaVA 1.5 7B model:
bash launch/agg_multimodal_epd.sh --model llava-hf/llava-1.5-7b-hf
# Serve a Qwen2.5-VL model:
bash launch/agg_multimodal_epd.sh --model Qwen/Qwen2.5-VL-7B-Instruct
```
**Client:**
```bash
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "llava-hf/llava-1.5-7b-hf",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "What is in this image?"
},
{
"type": "image_url",
"image_url": {
"url": "http://images.cocodataset.org/test2017/000000155781.jpg"
}
}
]
}
],
"max_tokens": 300,
"temperature": 0.0,
"stream": false
}'
```
### E/P/D Serving (Full Disaggregation)
**Components:**
- workers: [EncodeWorkerHandler](../../components/src/dynamo/vllm/multimodal_handlers/encode_worker_handler.py) for encoding, [MultimodalDecodeWorkerHandler](../../components/src/dynamo/vllm/multimodal_handlers/worker_handler.py) for decoding, and [MultimodalPDWorkerHandler](../../components/src/dynamo/vllm/multimodal_handlers/worker_handler.py) for prefilling.
- processor: Tokenizes the prompt and passes it to the EncodeWorkerHandler.
- frontend: HTTP endpoint to handle incoming requests.
**Workflow:**
For the LLaVA model, embeddings are only required during the prefill stage. The EncodeWorkerHandler is connected directly to the prefill worker, encoding the image and passing embeddings via NATS and RDMA. The prefill worker performs the prefilling step and forwards the KV cache to the decode worker.
```mermaid
flowchart LR
HTTP --> processor
processor --> HTTP
processor --image_url--> encode_worker
encode_worker --> processor
encode_worker --embeddings--> prefill_worker
prefill_worker --> encode_worker
prefill_worker --> decode_worker
decode_worker --> prefill_worker
```
**Launch:**
```bash
cd $DYNAMO_HOME/examples/backends/vllm
bash launch/disagg_multimodal_epd.sh --model llava-hf/llava-1.5-7b-hf
```
> [!NOTE] Disaggregation is currently only confirmed to work with LLaVA. Qwen2.5-VL is not confirmed to be supported.
## ECConnector Serving
ECConnector is vLLM's native connector for transferring multimodal embeddings via an Embedding Cache. The encoder worker acts as a **producer** (writes embeddings), while the PD worker acts as a **consumer** (reads embeddings).
**Workflow:**
```mermaid
flowchart LR
HTTP --> processor[EC Processor]
processor --image_url--> encoder[vLLM Native Encoder<br/>Producer]
encoder --writes--> cache[(Embedding Cache)]
cache --reads--> pd[PD Worker<br/>Consumer]
pd --> processor
processor --> HTTP
```
**Launch:**
```bash
cd $DYNAMO_HOME/examples/backends/vllm
bash launch/agg_multimodal_ec_connector.sh --model llava-hf/llava-1.5-7b-hf
# Custom storage path for Embedding Cache
bash launch/agg_multimodal_ec_connector.sh --ec-storage-path /shared/encoder-cache
```
**Client:** Same as [E/PD Serving](#epd-serving-encode-separate)
## Llama 4 Serving
The Llama 4 model family is natively multimodal. Unlike LLaVA, they do not directly consume image embeddings as input (see the [vLLM support matrix](https://docs.vllm.ai/en/latest/models/supported_models.html#text-generation_1)). Therefore, the encoder worker is not used and encoding is done alongside prefill.
Example model: `meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8` on H100x8.
### Llama 4 Aggregated Serving
**Workflow:**
```mermaid
flowchart LR
HTTP --> processor
processor --> HTTP
processor --image_url--> pd_worker
pd_worker --> processor
```
**Launch:**
```bash
cd $DYNAMO_HOME/examples/backends/vllm
bash launch/agg_multimodal.sh --model meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8
```
**Client:**
```bash
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "What is in this image?"
},
{
"type": "image_url",
"image_url": {
"url": "http://images.cocodataset.org/test2017/000000155781.jpg"
}
}
]
}
],
"max_tokens": 300,
"temperature": 0.0,
"stream": false
}'
```
### Llama 4 Disaggregated Serving
**Workflow:**
```mermaid
flowchart LR
HTTP --> processor
processor --> HTTP
processor --image_url--> prefill_worker
prefill_worker --> processor
prefill_worker --> decode_worker
decode_worker --> prefill_worker
```
**Launch:**
```bash
cd $DYNAMO_HOME/examples/backends/vllm
bash launch/disagg_multimodal_llama.sh --head-node
# On a separate node with NATS_SERVER and ETCD_ENDPOINTS pointing to head node:
cd $DYNAMO_HOME/examples/backends/vllm
bash launch/disagg_multimodal_llama.sh
```
## Video Serving
### Video Aggregated Serving
**Components:**
- workers: [VideoEncodeWorker](../../examples/multimodal/components/video_encode_worker.py) for decoding video into frames, and [VllmPDWorker](../../examples/multimodal/components/worker.py) for prefilling and decoding.
- processor: Tokenizes the prompt and passes it to the VideoEncodeWorker.
- frontend: HTTP endpoint to handle incoming requests.
**Workflow:**
The VideoEncodeWorker decodes the video into frames. Unlike the image pipeline which generates embeddings, this pipeline passes raw frames directly to the VllmPDWorker via NATS and RDMA.
```mermaid
flowchart LR
HTTP --> processor
processor --> HTTP
processor --video_url--> video_encode_worker
video_encode_worker --> processor
video_encode_worker --frames--> pd_worker
pd_worker --> video_encode_worker
```
**Launch:**
```bash
cd $DYNAMO_HOME/examples/multimodal
bash launch/video_agg.sh
```
**Client:**
```bash
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "llava-hf/LLaVA-NeXT-Video-7B-hf",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Describe the video in detail"
},
{
"type": "video_url",
"video_url": {
"url": "https://storage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
}
}
]
}
],
"max_tokens": 300,
"stream": false
}' | jq
```
### Video Disaggregated Serving
**Workflow:**
For the LLaVA-NeXT-Video-7B model, frames are only required during the prefill stage. The VideoEncodeWorker is connected directly to the prefill worker, decoding the video into frames and passing them via RDMA.
```mermaid
flowchart LR
HTTP --> processor
processor --> HTTP
processor --video_url--> video_encode_worker
video_encode_worker --> processor
video_encode_worker --frames--> prefill_worker
prefill_worker --> video_encode_worker
prefill_worker --> decode_worker
decode_worker --> prefill_worker
```
**Launch:**
```bash
cd $DYNAMO_HOME/examples/multimodal
bash launch/video_disagg.sh
```
## Audio Serving
### Audio Aggregated Serving
**Components:**
- workers: [AudioEncodeWorker](../../examples/multimodal/components/audio_encode_worker.py) for decoding audio into embeddings, and [VllmPDWorker](../../examples/multimodal/components/worker.py) for prefilling and decoding.
- processor: Tokenizes the prompt and passes it to the AudioEncodeWorker.
- frontend: HTTP endpoint to handle incoming requests.
**Workflow:**
```mermaid
flowchart LR
HTTP --> processor
processor --> HTTP
processor --audio_url--> audio_encode_worker
audio_encode_worker --> processor
audio_encode_worker --embeddings--> pd_worker
pd_worker --> audio_encode_worker
```
**Launch:**
```bash
pip install 'vllm[audio]' accelerate # multimodal audio models dependency
cd $DYNAMO_HOME/examples/multimodal
bash launch/audio_agg.sh
```
**Client:**
```bash
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen2-Audio-7B-Instruct",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "What is recited in the audio?"
},
{
"type": "audio_url",
"audio_url": {
"url": "https://raw.githubusercontent.com/yuekaizhang/Triton-ASR-Client/main/datasets/mini_en/wav/1221-135766-0002.wav"
}
}
]
}
],
"max_tokens": 6000,
"temperature": 0.8,
"stream": false
}' | jq
```
### Audio Disaggregated Serving
**Workflow:**
For the Qwen2-Audio model, audio embeddings are only required during the prefill stage. The AudioEncodeWorker is connected directly to the prefill worker.
```mermaid
flowchart LR
HTTP --> processor
processor --> HTTP
processor --audio_url--> audio_encode_worker
audio_encode_worker --> processor
audio_encode_worker --embeddings--> prefill_worker
prefill_worker --> audio_encode_worker
prefill_worker --> decode_worker
decode_worker --> prefill_worker
```
**Launch:**
```bash
pip install 'vllm[audio]' accelerate # multimodal audio models dependency
cd $DYNAMO_HOME/examples/multimodal
bash launch/audio_disagg.sh
```
## NIXL Usage
| Use Case | Script | NIXL Used? | Data Transfer |
|----------|--------|------------|---------------|
| EPD (Simple Aggregated) | `agg_multimodal.sh` | No | All in one worker |
| E/PD (Encode Separate) | `agg_multimodal_epd.sh` | Yes | Encoder → PD (embeddings) |
| E/P/D (Full Disaggregation) | `disagg_multimodal_epd.sh` | Yes | Encoder → Prefill (embeddings), Prefill → Decode (KV cache) |
| EP/D (Llama 4) | `disagg_multimodal_llama.sh` | Yes | Prefill → Decode (KV cache) |
| E/PD (EC Connector) | `agg_multimodal_ec_connector.sh` | No | ECConnector via Embedding Cache |
## ModelInput Types and Registration
Dynamo's Rust SDK supports two input types that determine how the HTTP frontend preprocesses requests:
| ModelInput Type | Preprocessing | Use Case |
|-----------------|---------------|----------|
| `ModelInput.Text` | None (raw text passed through) | Components that tokenize themselves |
| `ModelInput.Tokens` | Rust SDK would tokenize (but bypassed in multimodal) | Components expecting pre-tokenized input |
**Registration Pattern:**
```python
# Processor - Entry point from HTTP frontend
await register_llm(
ModelInput.Text, # Frontend sends raw text
ModelType.Chat,
generate_endpoint,
model_name,
...
)
# Workers - Internal components
await register_llm(
ModelInput.Tokens, # Expect pre-tokenized input
ModelType.Chat, # or ModelType.Prefill for prefill workers
generate_endpoint,
model_name,
...
)
```
## Known Limitations
- **Disaggregated flows require Python Processor** - All multimodal disaggregation requires the Python Processor component (`ModelInput.Text`).
## Supported Models
The following models have been tested with Dynamo's vLLM multimodal backend:
- **Qwen2.5-VL** - `Qwen/Qwen2.5-VL-7B-Instruct`
- **Qwen3-VL** - `Qwen/Qwen3-VL-30B-A3B-Instruct-FP8`
- **LLaVA 1.5** - `llava-hf/llava-1.5-7b-hf`
- **Llama 4 Maverick** - `meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8`
- **LLaVA Next Video** - `llava-hf/LLaVA-NeXT-Video-7B-hf`
- **Qwen2-Audio** - `Qwen/Qwen2-Audio-7B-Instruct`
For a complete list of multimodal models supported by vLLM, see [vLLM Supported Multimodal Models](https://docs.vllm.ai/en/latest/models/supported_models/#list-of-multimodal-language-models). Models listed there should work with Simple Aggregated Mode but may not be explicitly tested.
## Key Files
| File | Description |
|------|-------------|
| `components/src/dynamo/vllm/main.py` | Worker initialization and setup |
| `components/src/dynamo/vllm/args.py` | Command-line argument parsing |
| `components/src/dynamo/vllm/multimodal_handlers/processor_handler.py` | Processor implementation |
| `components/src/dynamo/vllm/multimodal_handlers/encode_worker_handler.py` | Encode worker implementations (custom and vLLM-native) |
| `components/src/dynamo/vllm/multimodal_handlers/worker_handler.py` | PD/Prefill/Decode worker implementation |
<!--
SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
# Speculative Decoding
Speculative decoding is an optimization technique that uses a smaller "draft" model to predict multiple tokens, which are then verified by the main model in parallel. This can significantly reduce latency for autoregressive generation.
## Backend Support
| Backend | Status | Notes |
|---------|--------|-------|
| vLLM | ✅ | Eagle3 draft model support |
| SGLang | 🚧 | Not yet documented |
| TensorRT-LLM | 🚧 | Not yet documented |
## Overview
Speculative decoding works by:
1. **Draft phase**: A smaller, faster model generates candidate tokens
2. **Verify phase**: The main model verifies these candidates in a single forward pass
3. **Accept/reject**: Tokens are accepted if they match what the main model would have generated
This approach trades off additional compute for lower latency, as multiple tokens can be generated per forward pass of the main model.
## Quick Start (vLLM + Eagle3)
This guide walks through deploying **Meta-Llama-3.1-8B-Instruct** with **Eagle3** speculative decoding on a single GPU with at least 16GB VRAM.
### Prerequisites
1. Start infrastructure services:
```bash
docker compose -f deploy/docker-compose.yml up -d
```
2. Build and run the vLLM container:
```bash
./container/build.sh --framework VLLM
./container/run.sh -it --framework VLLM --mount-workspace
```
3. Set up Hugging Face access (Meta-Llama-3.1-8B-Instruct is gated):
```bash
export HUGGING_FACE_HUB_TOKEN="your_token_here"
export HF_TOKEN=$HUGGING_FACE_HUB_TOKEN
```
### Run Speculative Decoding
```bash
cd examples/backends/vllm
bash launch/agg_spec_decoding.sh
```
### Test the Deployment
```bash
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "meta-llama/Meta-Llama-3.1-8B-Instruct",
"messages": [
{"role": "user", "content": "Write a poem about why Sakura trees are beautiful."}
],
"max_tokens": 250
}'
```
## Backend-Specific Guides
| Backend | Guide |
|---------|-------|
| vLLM | [speculative_decoding_vllm.md](./speculative_decoding_vllm.md) |
## See Also
- [vLLM Backend](../../backends/vllm/README.md) - Full vLLM deployment guide
- [Disaggregated Serving](../../design_docs/disagg_serving.md) - Alternative optimization approach
- [Meta-Llama-3.1-8B-Instruct on Hugging Face](https://huggingface.co/meta-llama/Llama-3.1-8B-Instruct)
<!--
SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
# Speculative Decoding with vLLM
Using Speculative Decoding with the vLLM backend.
> **See also**: [Speculative Decoding Overview](./README.md) for cross-backend documentation.
## Prerequisites
- vLLM container with Eagle3 support
- GPU with at least 16GB VRAM
- Hugging Face access token (for gated models)
## Quick Start: Meta-Llama-3.1-8B-Instruct + Eagle3
This guide walks through deploying **Meta-Llama-3.1-8B-Instruct** with **Eagle3** speculative decoding on a single node.
### Step 1: Set Up Your Docker Environment
First, initialize a Docker container using the vLLM backend. See the [vLLM Quickstart Guide](../../backends/vllm/README.md#vllm-quick-start) for details.
```bash
# Launch infrastructure services
docker compose -f deploy/docker-compose.yml up -d
# Build the container
./container/build.sh --framework VLLM
# Run the container
./container/run.sh -it --framework VLLM --mount-workspace
```
### Step 2: Get Access to the Llama-3 Model
The **Meta-Llama-3.1-8B-Instruct** model is gated. Request access on Hugging Face:
[Meta-Llama-3.1-8B-Instruct repository](https://huggingface.co/meta-llama/Llama-3.1-8B-Instruct)
Approval time varies depending on Hugging Face review traffic.
Once approved, set your access token inside the container:
```bash
export HUGGING_FACE_HUB_TOKEN="insert_your_token_here"
export HF_TOKEN=$HUGGING_FACE_HUB_TOKEN
```
### Step 3: Run Aggregated Speculative Decoding
```bash
# Requires only one GPU
cd examples/backends/vllm
bash launch/agg_spec_decoding.sh
```
Once the weights finish downloading, the server will be ready for inference requests.
### Step 4: Test the Deployment
```bash
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "meta-llama/Meta-Llama-3.1-8B-Instruct",
"messages": [
{"role": "user", "content": "Write a poem about why Sakura trees are beautiful."}
],
"max_tokens": 250
}'
```
### Example Output
```json
{
"id": "cmpl-3e87ea5c-010e-4dd2-bcc4-3298ebd845a8",
"choices": [
{
"message": {
"role": "assistant",
"content": "In cherry blossom's gentle breeze ... A delicate balance of life and death, as petals fade, and new life breathes."
},
"index": 0,
"finish_reason": "stop"
}
],
"model": "meta-llama/Meta-Llama-3.1-8B-Instruct",
"usage": {
"prompt_tokens": 16,
"completion_tokens": 250,
"total_tokens": 266
}
}
```
## Configuration
Speculative decoding in vLLM uses Eagle3 as the draft model. The launch script configures:
- Target model: `meta-llama/Meta-Llama-3.1-8B-Instruct`
- Draft model: Eagle3 variant
- Aggregated serving mode
See `examples/backends/vllm/launch/agg_spec_decoding.sh` for the full configuration.
## Limitations
- Currently only supports Eagle3 as the draft model
- Requires compatible model architectures between target and draft
## See Also
| Document | Path |
|----------|------|
| Speculative Decoding Overview | [README.md](./README.md) |
| vLLM Backend Guide | [vLLM README](../../backends/vllm/README.md) |
| Meta-Llama-3.1-8B-Instruct | [Hugging Face](https://huggingface.co/meta-llama/Llama-3.1-8B-Instruct) |
#!/usr/bin/env python3
# type: ignore # Ignore all mypy errors in this file
# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import logging
import os
import re
import subprocess
from contextlib import contextmanager
from functools import partial
# Get the directory of the current file
dynamo_docs_abspath = os.path.dirname(os.path.abspath(__file__))
dynamo_abspath = os.path.dirname(dynamo_docs_abspath)
repo_url = "https://github.com/ai-dynamo/dynamo/blob/main/"
# Regex patterns
http_patn = r"^https?://"
http_reg = re.compile(http_patn)
tag_patn = "/(?:blob|tree)/main"
dynamo_repo_patn = rf"{http_patn}github.com/ai-dynamo/dynamo"
dynamo_github_url_reg = re.compile(
rf"{dynamo_repo_patn}/([^/#]+)(?:{tag_patn})?/*([^#]*)\s*(?=#|$)"
)
# relpath_patn = r"]\s*\(\s*([^)]+)\)"
# Hyperlink in a .md file, excluding embedded images.
# Uses greedy [^)]+ instead of lazy [^)]+? to avoid backtracking with \s*
hyperlink_reg = re.compile(r"((?<!\!)\[[^\]]+\]\s*\(\s*)([^)]+)(\))")
exclusions = None
with open(f"{dynamo_docs_abspath}/exclusions.txt", "r") as f:
exclusions = f.read()
f.close()
exclude_patterns = exclusions.strip().split("\n")
def setup_logger():
"""
This function is to setup logging
"""
# Create a custom logger
logger = logging.getLogger(__name__)
# Set the log level
logger.setLevel(logging.INFO)
# Create handlers
stream_handler = logging.StreamHandler()
# Create formatters and add it to the handlers
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
stream_handler.setFormatter(formatter)
logger.addHandler(stream_handler)
return logger
def log_message(message):
"""
This function is for logging to /tmp
- message: Message to log
"""
# Setup the logger
logger = setup_logger()
# Log the message
logger.info(message)
def run_command(command):
"""
This function runs any command using subprocess and logs failures
- command: Command to execute
"""
log_message(f"Running command: {command}")
try:
subprocess.run(
command,
shell=True,
check=True,
text=True,
capture_output=False,
)
except subprocess.CalledProcessError as e:
raise (e)
def is_excluded(file_path):
for exclude_pattern in exclude_patterns:
file_abspath = os.path.abspath(file_path)
exclude_pattern = os.path.abspath(exclude_pattern)
if os.path.commonpath([file_abspath, exclude_pattern]) == exclude_pattern:
return True
return False
def replace_url_with_relpath(url, src_doc_path):
"""
This function replaces Triton Inference Server GitHub URLs with relative paths in following cases.
1. URL is a doc file, e.g. ".md" file.
2. URL is a directory which contains README.md and URL ends with "#<section>".
Examples:
https://github.com/triton-inference-server/server/blob/main/docs/protocol#restricted-protocols
https://github.com/triton-inference-server/server/blob/main/docs/protocol/extension_shared_memory.md
https://github.com/triton-inference-server/server/blob/main/docs/user_guide/model_configuration.md#dynamic-batcher
Keep URL in the following cases:
https://github.com/triton-inference-server/server/tree/r24.02
https://github.com/triton-inference-server/server/blob/main/build.py
https://github.com/triton-inference-server/server/blob/main/qa
https://github.com/triton-inference-server/server/blob/main/CONTRIBUTING.md
"""
m = dynamo_github_url_reg.match(url)
# Do not replace URL if it is not a Triton GitHub file.
if not m:
return url
target_repo_name = m.group(1)
target_relpath_from_target_repo = os.path.normpath(m.groups("")[1])
section = url[len(m.group(0)) :]
valid_hashtag = section not in ["", "#"] and section.startswith("#")
if target_repo_name == "dynamo":
target_path = os.path.join(dynamo_abspath, target_relpath_from_target_repo)
else:
target_path = os.path.join(
dynamo_docs_abspath, target_repo_name, target_relpath_from_target_repo
)
# Return URL if it points to a path outside server/docs.
if os.path.commonpath([dynamo_docs_abspath, target_path]) != dynamo_docs_abspath:
return url
if (
os.path.isfile(target_path)
and os.path.splitext(target_path)[1] == ".md"
and not is_excluded(target_path)
):
pass
elif (
os.path.isdir(target_path)
and os.path.isfile(os.path.join(target_path, "README.md"))
and valid_hashtag
and not is_excluded(os.path.join(target_path, "README.md"))
):
target_path = os.path.join(target_path, "README.md")
else:
return url
# The "target_path" must be a file at this line.
relpath = os.path.relpath(target_path, start=os.path.dirname(src_doc_path))
return re.sub(dynamo_github_url_reg, relpath, url, count=1)
def replace_relpath_with_url(relpath, src_doc_path):
"""
This function replaces relative paths with Triton Inference Server GitHub URLs in following cases.
1. Relative path is a file that is not ".md" type inside the current repo.
2. Relative path is a directory but not (has "README.md" and ends with "#<section>").
3. Relative path does not exist (shows 404 page).
Examples:
../examples/model_repository
../examples/model_repository/inception_graphdef/config.pbtxt
Keep relpath in the following cases:
build.md
build.md#building-with-docker
#building-with-docker
../getting_started/quickstart.md
../protocol#restricted-protocols
"""
target_path = relpath.rsplit("#")[0]
section = relpath[len(target_path) :]
valid_hashtag = section not in ["", "#"]
if relpath.startswith("#"):
target_path = os.path.basename(src_doc_path)
target_path = os.path.join(os.path.dirname(src_doc_path), target_path)
target_path = os.path.normpath(target_path)
# Assert target path is under the current repo directory.
assert os.path.commonpath([dynamo_abspath, target_path]) == dynamo_abspath
target_path_from_src_repo = os.path.relpath(target_path, start=dynamo_abspath)
# For example, target_path of "../protocol#restricted-protocols" should be "<path-to-server>/server/docs/protocol/README.md"
if (
os.path.isdir(target_path)
and valid_hashtag
and os.path.isfile(os.path.join(target_path, "README.md"))
):
relpath = os.path.join(relpath.rsplit("#")[0], "README.md") + section
target_path = os.path.join(target_path, "README.md")
if (
os.path.isfile(target_path)
and os.path.splitext(target_path)[1] == ".md"
and os.path.commonpath([dynamo_docs_abspath, target_path])
== dynamo_docs_abspath
and not is_excluded(target_path)
):
return relpath
else:
return repo_url + target_path_from_src_repo + section
def replace_hyperlink(m, src_doc_path):
"""
TODO: Support of HTML tags for future docs.
Markdown allows <link>, e.g. <a href=[^>]+>. Whether we want to
find and replace the link depends on if they link to internal .md files
or allows relative paths. I haven't seen one such case in our doc so
should be safe for now.
"""
# Strip trailing whitespace since the greedy regex includes it
hyperlink_str = m.group(2).rstrip()
match = http_reg.match(hyperlink_str)
if match:
# Hyperlink is a URL.
res = replace_url_with_relpath(hyperlink_str, src_doc_path)
else:
# Hyperlink is a relative path.
res = replace_relpath_with_url(hyperlink_str, src_doc_path)
return m.group(1) + res + m.group(3)
def preprocess_docs(exclude_paths=[]):
# Find all ".md" files inside the current repo.
if exclude_paths:
cmd = (
["find", dynamo_docs_abspath, "-type", "d", "\\("]
+ " -o ".join([f"-path './{dir}'" for dir in exclude_paths]).split(" ")
+ ["\\)", "-prune", "-o", "-type", "f", "-name", "'*.md'", "-print"]
)
else:
cmd = ["find", dynamo_docs_abspath, "-name", "'*.md'"]
cmd = " ".join(cmd)
result = subprocess.run(cmd, check=True, capture_output=True, text=True, shell=True)
docs_list = list(filter(None, result.stdout.split("\n")))
# Read, preprocess and write back to each document file.
for doc_abspath in docs_list:
if is_excluded(doc_abspath):
continue
content = None
with open(doc_abspath, "r") as f:
content = f.read()
content = hyperlink_reg.sub(
partial(replace_hyperlink, src_doc_path=doc_abspath),
content,
)
with open(doc_abspath, "w") as f:
f.write(content)
@contextmanager
def change_directory(path):
"""
Context manager for changing the current working directory
"""
original_directory = os.getcwd()
try:
os.chdir(path)
yield
finally:
os.chdir(original_directory)
def update_project_json():
"""Update project.json with the current version from DYNAMO_DOCS_VERSION env var."""
version = os.environ.get("DYNAMO_DOCS_VERSION", "dev")
project_json_path = os.path.join(dynamo_docs_abspath, "project.json")
project_data = {"name": "NVIDIA Dynamo", "version": version}
with open(project_json_path, "w") as f:
json.dump(project_data, f)
log_message(f"Updated project.json with version: {version}")
def main():
with change_directory(dynamo_docs_abspath):
run_command("make clean")
update_project_json()
preprocess_docs()
run_command("make html")
if __name__ == "__main__":
main()
:orphan:
..
SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0
.. This hidden toctree includes readmes etc that aren't meant to be in the main table of contents but should be accounted for in the sphinx project structure
.. toctree::
:maxdepth: 2
:hidden:
development/runtime-guide.md
development/jail_stream.md
api/nixl_connect/connector.md
api/nixl_connect/descriptor.md
api/nixl_connect/device.md
api/nixl_connect/device_kind.md
api/nixl_connect/operation_status.md
api/nixl_connect/rdma_metadata.md
api/nixl_connect/readable_operation.md
api/nixl_connect/writable_operation.md
api/nixl_connect/read_operation.md
api/nixl_connect/write_operation.md
api/nixl_connect/README.md
kubernetes/api_reference.md
kubernetes/deployment/create_deployment.md
kubernetes/chrek/dynamo.md
kubernetes/chrek/standalone.md
kubernetes/fluxcd.md
kubernetes/model_caching_with_fluid.md
reference/cli.md
reference/glossary.md
performance/tuning.md
backends/vllm/deepseek-r1.md
backends/vllm/gpt-oss.md
backends/vllm/multi-node.md
backends/vllm/prometheus.md
backends/vllm/prompt-embeddings.md
backends/vllm/vllm-omni.md
backends/sglang/expert-distribution-eplb.md
backends/sglang/gpt-oss.md
backends/sglang/diffusion-lm.md
backends/sglang/profiling.md
backends/sglang/sglang-disaggregation.md
backends/sglang/prometheus.md
backends/trtllm/multinode/multinode-examples.md
backends/trtllm/llama4_plus_eagle.md
backends/trtllm/kv-cache-transfer.md
backends/trtllm/gemma3_sliding_window_attention.md
backends/trtllm/gpt-oss.md
backends/trtllm/prometheus.md
features/speculative_decoding/README.md
features/speculative_decoding/speculative_decoding_vllm.md
examples/README.md
examples/runtime/hello_world/README.md
benchmarks/kv-router-ab-testing.md
mocker/mocker.md
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" data-d2-version="0.7.1" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1667 982"><svg class="d2-3076236447 d2-svg" width="1667" height="982" viewBox="-19 -19 1667 982"><rect x="-19.000000" y="-19.000000" width="1667.000000" height="982.000000" rx="0.000000" fill="transparent" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-3076236447 .text {
font-family: "d2-3076236447-font-regular";
}
@font-face {
font-family: d2-3076236447-font-regular;
src: url("data:application/font-woff;base64,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");
}
@font-face {
font-family: d2-3076236447-font-semibold;
src: url("data:application/font-woff;base64,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");
}
.d2-3076236447 .text-mono {
font-family: "d2-3076236447-font-mono";
}
@font-face {
font-family: d2-3076236447-font-mono;
src: url("data:application/font-woff;base64,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");
}
.d2-3076236447 .text-mono-bold {
font-family: "d2-3076236447-font-mono-bold";
}
@font-face {
font-family: d2-3076236447-font-mono-bold;
src: url("data:application/font-woff;base64,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");
}
.d2-3076236447 .text-mono-italic {
font-family: "d2-3076236447-font-mono-italic";
}
@font-face {
font-family: d2-3076236447-font-mono-italic;
src: url("data:application/font-woff;base64,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");
}
@media (prefers-color-scheme: dark) {
.d2-3076236447 .fill-N1 { fill: #E8E8E8; }
.d2-3076236447 .fill-N2 { fill: #CCCCCC; }
.d2-3076236447 .fill-N3 { fill: #999999; }
.d2-3076236447 .fill-B1 { fill: #E8E8E8; }
.d2-3076236447 .fill-B2 { fill: #7BACFF; }
.d2-3076236447 .fill-B3 { fill: #6AAFDC; }
.d2-3076236447 .fill-N4 { fill: #3A3A44; }
.d2-3076236447 .fill-N5 { fill: #2E2E38; }
.d2-3076236447 .fill-N6 { fill: #252530; }
.d2-3076236447 .fill-N7 { fill: transparent; }
.d2-3076236447 .fill-B4 { fill: #2A2A34; }
.d2-3076236447 .fill-B5 { fill: #1E1E28; }
.d2-3076236447 .fill-B6 { fill: #16161E; }
.d2-3076236447 .stroke-N1 { stroke: #E8E8E8; }
.d2-3076236447 .stroke-N2 { stroke: #CCCCCC; }
.d2-3076236447 .stroke-N3 { stroke: #999999; }
.d2-3076236447 .stroke-N4 { stroke: #555555; }
.d2-3076236447 .stroke-N5 { stroke: #444444; }
.d2-3076236447 .stroke-N6 { stroke: #333333; }
.d2-3076236447 .stroke-N7 { stroke: transparent; }
.d2-3076236447 .stroke-B1 { stroke: #E8E8E8; }
.d2-3076236447 .stroke-B2 { stroke: #7BACFF; }
.d2-3076236447 .stroke-B3 { stroke: #6AAFDC; }
.d2-3076236447 .stroke-B4 { stroke: #3A3A44; }
.d2-3076236447 .stroke-B5 { stroke: #2E2E38; }
.d2-3076236447 .stroke-B6 { stroke: #252530; }
.d2-3076236447 .color-N1 { color: #E8E8E8; }
.d2-3076236447 .color-N2 { color: #CCCCCC; }
.d2-3076236447 .color-N3 { color: #999999; }
.d2-3076236447 .connection.fill-B1 { fill: #E8E8E8; }
}
]]></style><style type="text/css"><![CDATA[.shape {
shape-rendering: geometricPrecision;
stroke-linejoin: round;
}
.connection {
stroke-linecap: round;
stroke-linejoin: round;
}
.blend {
mix-blend-mode: multiply;
opacity: 0.5;
}
.d2-3076236447 .fill-N1{fill:#000410;}
.d2-3076236447 .fill-N2{fill:#0000B8;}
.d2-3076236447 .fill-N3{fill:#9499AB;}
.d2-3076236447 .fill-N4{fill:#CFD2DD;}
.d2-3076236447 .fill-N5{fill:#C3DEF3;}
.d2-3076236447 .fill-N6{fill:#EEF1F8;}
.d2-3076236447 .fill-N7{fill:#FFFFFF;}
.d2-3076236447 .fill-B1{fill:#000410;}
.d2-3076236447 .fill-B2{fill:#0000E4;}
.d2-3076236447 .fill-B3{fill:#5AA4DC;}
.d2-3076236447 .fill-B4{fill:#E7E9EE;}
.d2-3076236447 .fill-B5{fill:#F5F6F9;}
.d2-3076236447 .fill-B6{fill:#FFFFFF;}
.d2-3076236447 .fill-AA2{fill:#008566;}
.d2-3076236447 .fill-AA4{fill:#45BBA5;}
.d2-3076236447 .fill-AA5{fill:#7ACCBD;}
.d2-3076236447 .fill-AB4{fill:#F1C759;}
.d2-3076236447 .fill-AB5{fill:#F9E088;}
.d2-3076236447 .stroke-N1{stroke:#000410;}
.d2-3076236447 .stroke-N2{stroke:#0000B8;}
.d2-3076236447 .stroke-N3{stroke:#9499AB;}
.d2-3076236447 .stroke-N4{stroke:#CFD2DD;}
.d2-3076236447 .stroke-N5{stroke:#C3DEF3;}
.d2-3076236447 .stroke-N6{stroke:#EEF1F8;}
.d2-3076236447 .stroke-N7{stroke:#FFFFFF;}
.d2-3076236447 .stroke-B1{stroke:#000410;}
.d2-3076236447 .stroke-B2{stroke:#0000E4;}
.d2-3076236447 .stroke-B3{stroke:#5AA4DC;}
.d2-3076236447 .stroke-B4{stroke:#E7E9EE;}
.d2-3076236447 .stroke-B5{stroke:#F5F6F9;}
.d2-3076236447 .stroke-B6{stroke:#FFFFFF;}
.d2-3076236447 .stroke-AA2{stroke:#008566;}
.d2-3076236447 .stroke-AA4{stroke:#45BBA5;}
.d2-3076236447 .stroke-AA5{stroke:#7ACCBD;}
.d2-3076236447 .stroke-AB4{stroke:#F1C759;}
.d2-3076236447 .stroke-AB5{stroke:#F9E088;}
.d2-3076236447 .background-color-N1{background-color:#000410;}
.d2-3076236447 .background-color-N2{background-color:#0000B8;}
.d2-3076236447 .background-color-N3{background-color:#9499AB;}
.d2-3076236447 .background-color-N4{background-color:#CFD2DD;}
.d2-3076236447 .background-color-N5{background-color:#C3DEF3;}
.d2-3076236447 .background-color-N6{background-color:#EEF1F8;}
.d2-3076236447 .background-color-N7{background-color:#FFFFFF;}
.d2-3076236447 .background-color-B1{background-color:#000410;}
.d2-3076236447 .background-color-B2{background-color:#0000E4;}
.d2-3076236447 .background-color-B3{background-color:#5AA4DC;}
.d2-3076236447 .background-color-B4{background-color:#E7E9EE;}
.d2-3076236447 .background-color-B5{background-color:#F5F6F9;}
.d2-3076236447 .background-color-B6{background-color:#FFFFFF;}
.d2-3076236447 .background-color-AA2{background-color:#008566;}
.d2-3076236447 .background-color-AA4{background-color:#45BBA5;}
.d2-3076236447 .background-color-AA5{background-color:#7ACCBD;}
.d2-3076236447 .background-color-AB4{background-color:#F1C759;}
.d2-3076236447 .background-color-AB5{background-color:#F9E088;}
.d2-3076236447 .color-N1{color:#000410;}
.d2-3076236447 .color-N2{color:#0000B8;}
.d2-3076236447 .color-N3{color:#9499AB;}
.d2-3076236447 .color-N4{color:#CFD2DD;}
.d2-3076236447 .color-N5{color:#C3DEF3;}
.d2-3076236447 .color-N6{color:#EEF1F8;}
.d2-3076236447 .color-N7{color:#FFFFFF;}
.d2-3076236447 .color-B1{color:#000410;}
.d2-3076236447 .color-B2{color:#0000E4;}
.d2-3076236447 .color-B3{color:#5AA4DC;}
.d2-3076236447 .color-B4{color:#E7E9EE;}
.d2-3076236447 .color-B5{color:#F5F6F9;}
.d2-3076236447 .color-B6{color:#FFFFFF;}
.d2-3076236447 .color-AA2{color:#008566;}
.d2-3076236447 .color-AA4{color:#45BBA5;}
.d2-3076236447 .color-AA5{color:#7ACCBD;}
.d2-3076236447 .color-AB4{color:#F1C759;}
.d2-3076236447 .color-AB5{color:#F9E088;}.appendix text.text{fill:#000410}.md{--color-fg-default:#000410;--color-fg-muted:#0000B8;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#000410;--color-border-muted:#0000E4;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0000E4;--color-accent-emphasis:#0000E4;--color-attention-subtle:#0000B8;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker-d2-3076236447);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker-d2-3076236447);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-normal-d2-3076236447);mix-blend-mode:color-burn}.sketch-overlay-B4{fill:url(#streaks-bright-d2-3076236447);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright-d2-3076236447);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright-d2-3076236447);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark-d2-3076236447);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-normal-d2-3076236447);mix-blend-mode:color-burn}.sketch-overlay-AA5{fill:url(#streaks-normal-d2-3076236447);mix-blend-mode:color-burn}.sketch-overlay-AB4{fill:url(#streaks-normal-d2-3076236447);mix-blend-mode:color-burn}.sketch-overlay-AB5{fill:url(#streaks-normal-d2-3076236447);mix-blend-mode:color-burn}.sketch-overlay-N1{fill:url(#streaks-darker-d2-3076236447);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-darker-d2-3076236447);mix-blend-mode:lighten}.sketch-overlay-N3{fill:url(#streaks-normal-d2-3076236447);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal-d2-3076236447);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-normal-d2-3076236447);mix-blend-mode:color-burn}.sketch-overlay-N6{fill:url(#streaks-bright-d2-3076236447);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright-d2-3076236447);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}
@media (prefers-color-scheme: dark) {
.d2-3076236447 .fill-N1 { fill: #E8E8E8; }
.d2-3076236447 .fill-N2 { fill: #CCCCCC; }
.d2-3076236447 .fill-N3 { fill: #999999; }
.d2-3076236447 .fill-B1 { fill: #E8E8E8; }
.d2-3076236447 .fill-B2 { fill: #7BACFF; }
.d2-3076236447 .fill-B3 { fill: #6AAFDC; }
.d2-3076236447 .fill-N4 { fill: #3A3A44; }
.d2-3076236447 .fill-N5 { fill: #2E2E38; }
.d2-3076236447 .fill-N6 { fill: #252530; }
.d2-3076236447 .fill-N7 { fill: transparent; }
.d2-3076236447 .fill-B4 { fill: #2A2A34; }
.d2-3076236447 .fill-B5 { fill: #1E1E28; }
.d2-3076236447 .fill-B6 { fill: #16161E; }
.d2-3076236447 .stroke-N1 { stroke: #E8E8E8; }
.d2-3076236447 .stroke-N2 { stroke: #CCCCCC; }
.d2-3076236447 .stroke-N3 { stroke: #999999; }
.d2-3076236447 .stroke-N4 { stroke: #555555; }
.d2-3076236447 .stroke-N5 { stroke: #444444; }
.d2-3076236447 .stroke-N6 { stroke: #333333; }
.d2-3076236447 .stroke-N7 { stroke: transparent; }
.d2-3076236447 .stroke-B1 { stroke: #E8E8E8; }
.d2-3076236447 .stroke-B2 { stroke: #7BACFF; }
.d2-3076236447 .stroke-B3 { stroke: #6AAFDC; }
.d2-3076236447 .stroke-B4 { stroke: #3A3A44; }
.d2-3076236447 .stroke-B5 { stroke: #2E2E38; }
.d2-3076236447 .stroke-B6 { stroke: #252530; }
.d2-3076236447 .color-N1 { color: #E8E8E8; }
.d2-3076236447 .color-N2 { color: #CCCCCC; }
.d2-3076236447 .color-N3 { color: #999999; }
.d2-3076236447 .connection.fill-B1 { fill: #E8E8E8; }
}
]]></style><style type="text/css">.d2-3076236447 .md em,
.d2-3076236447 .md dfn {
font-family: "d2-3076236447-font-italic";
}
.d2-3076236447 .md b,
.d2-3076236447 .md strong {
font-family: "d2-3076236447-font-bold";
}
.d2-3076236447 .md code,
.d2-3076236447 .md kbd,
.d2-3076236447 .md pre,
.d2-3076236447 .md samp {
font-family: "d2-3076236447-font-mono";
font-size: 1em;
}
.d2-3076236447 .md {
tab-size: 4;
}
/* variables are provided in d2renderers/d2svg/d2svg.go */
.d2-3076236447 .md {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
margin: 0;
background-color: transparent; /* we don't want to define the background color */
font-family: "d2-3076236447-font-regular";
font-size: 16px;
line-height: 1.5;
word-wrap: break-word;
}
.d2-3076236447 .md details,
.d2-3076236447 .md figcaption,
.d2-3076236447 .md figure {
display: block;
}
.d2-3076236447 .md summary {
display: list-item;
}
.d2-3076236447 .md [hidden] {
display: none !important;
}
.d2-3076236447 .md a {
background-color: transparent;
color: var(--color-accent-fg);
text-decoration: none;
}
.d2-3076236447 .md a:active,
.d2-3076236447 .md a:hover {
outline-width: 0;
}
.d2-3076236447 .md abbr[title] {
border-bottom: none;
text-decoration: underline dotted;
}
.d2-3076236447 .md dfn {
font-style: italic;
}
.d2-3076236447 .md h1 {
margin: 0.67em 0;
padding-bottom: 0.3em;
font-size: 2em;
border-bottom: 1px solid var(--color-border-muted);
}
.d2-3076236447 .md mark {
background-color: var(--color-attention-subtle);
color: var(--color-text-primary);
}
.d2-3076236447 .md small {
font-size: 90%;
}
.d2-3076236447 .md sub,
.d2-3076236447 .md sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
.d2-3076236447 .md sub {
bottom: -0.25em;
}
.d2-3076236447 .md sup {
top: -0.5em;
}
.d2-3076236447 .md img {
border-style: none;
max-width: 100%;
box-sizing: content-box;
background-color: var(--color-canvas-default);
}
.d2-3076236447 .md figure {
margin: 1em 40px;
}
.d2-3076236447 .md hr {
box-sizing: content-box;
overflow: hidden;
background: transparent;
border-bottom: 1px solid var(--color-border-muted);
height: 0.25em;
padding: 0;
margin: 24px 0;
background-color: var(--color-border-default);
border: 0;
}
.d2-3076236447 .md input {
font: inherit;
margin: 0;
overflow: visible;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
.d2-3076236447 .md [type="button"],
.d2-3076236447 .md [type="reset"],
.d2-3076236447 .md [type="submit"] {
-webkit-appearance: button;
}
.d2-3076236447 .md [type="button"]::-moz-focus-inner,
.d2-3076236447 .md [type="reset"]::-moz-focus-inner,
.d2-3076236447 .md [type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
.d2-3076236447 .md [type="button"]:-moz-focusring,
.d2-3076236447 .md [type="reset"]:-moz-focusring,
.d2-3076236447 .md [type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
.d2-3076236447 .md [type="checkbox"],
.d2-3076236447 .md [type="radio"] {
box-sizing: border-box;
padding: 0;
}
.d2-3076236447 .md [type="number"]::-webkit-inner-spin-button,
.d2-3076236447 .md [type="number"]::-webkit-outer-spin-button {
height: auto;
}
.d2-3076236447 .md [type="search"] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
.d2-3076236447 .md [type="search"]::-webkit-search-cancel-button,
.d2-3076236447 .md [type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
.d2-3076236447 .md ::-webkit-input-placeholder {
color: inherit;
opacity: 0.54;
}
.d2-3076236447 .md ::-webkit-file-upload-button {
-webkit-appearance: button;
font: inherit;
}
.d2-3076236447 .md a:hover {
text-decoration: underline;
}
.d2-3076236447 .md hr::before {
display: table;
content: "";
}
.d2-3076236447 .md hr::after {
display: table;
clear: both;
content: "";
}
.d2-3076236447 .md table {
border-spacing: 0;
border-collapse: collapse;
display: block;
width: max-content;
max-width: 100%;
overflow: auto;
}
.d2-3076236447 .md td,
.d2-3076236447 .md th {
padding: 0;
}
.d2-3076236447 .md details summary {
cursor: pointer;
}
.d2-3076236447 .md details:not([open]) > *:not(summary) {
display: none !important;
}
.d2-3076236447 .md kbd {
display: inline-block;
padding: 3px 5px;
color: var(--color-fg-default);
vertical-align: middle;
background-color: var(--color-canvas-subtle);
border: solid 1px var(--color-neutral-muted);
border-bottom-color: var(--color-neutral-muted);
border-radius: 6px;
box-shadow: inset 0 -1px 0 var(--color-neutral-muted);
}
.d2-3076236447 .md h1,
.d2-3076236447 .md h2,
.d2-3076236447 .md h3,
.d2-3076236447 .md h4,
.d2-3076236447 .md h5,
.d2-3076236447 .md h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 400;
line-height: 1.25;
font-family: "d2-3076236447-font-semibold";
}
.d2-3076236447 .md h2 {
padding-bottom: 0.3em;
font-size: 1.5em;
border-bottom: 1px solid var(--color-border-muted);
}
.d2-3076236447 .md h3 {
font-size: 1.25em;
}
.d2-3076236447 .md h4 {
font-size: 1em;
}
.d2-3076236447 .md h5 {
font-size: 0.875em;
}
.d2-3076236447 .md h6 {
font-size: 0.85em;
color: var(--color-fg-muted);
}
.d2-3076236447 .md p {
margin-top: 0;
margin-bottom: 10px;
}
.d2-3076236447 .md blockquote {
margin: 0;
padding: 0 1em;
color: var(--color-fg-muted);
border-left: 0.25em solid var(--color-border-default);
}
.d2-3076236447 .md ul,
.d2-3076236447 .md ol {
margin-top: 0;
margin-bottom: 0;
padding-left: 2em;
}
.d2-3076236447 .md ol ol,
.d2-3076236447 .md ul ol {
list-style-type: lower-roman;
}
.d2-3076236447 .md ul ul ol,
.d2-3076236447 .md ul ol ol,
.d2-3076236447 .md ol ul ol,
.d2-3076236447 .md ol ol ol {
list-style-type: lower-alpha;
}
.d2-3076236447 .md dd {
margin-left: 0;
}
.d2-3076236447 .md pre {
margin-top: 0;
margin-bottom: 0;
word-wrap: normal;
}
.d2-3076236447 .md ::placeholder {
color: var(--color-fg-subtle);
opacity: 1;
}
.d2-3076236447 .md input::-webkit-outer-spin-button,
.d2-3076236447 .md input::-webkit-inner-spin-button {
margin: 0;
-webkit-appearance: none;
appearance: none;
}
.d2-3076236447 .md::before {
display: table;
content: "";
}
.d2-3076236447 .md::after {
display: table;
clear: both;
content: "";
}
.d2-3076236447 .md > *:first-child {
margin-top: 0 !important;
}
.d2-3076236447 .md > *:last-child {
margin-bottom: 0 !important;
}
.d2-3076236447 .md a:not([href]) {
color: inherit;
text-decoration: none;
}
.d2-3076236447 .md .absent {
color: var(--color-danger-fg);
}
.d2-3076236447 .md .anchor {
float: left;
padding-right: 4px;
margin-left: -20px;
line-height: 1;
}
.d2-3076236447 .md .anchor:focus {
outline: none;
}
.d2-3076236447 .md p,
.d2-3076236447 .md blockquote,
.d2-3076236447 .md ul,
.d2-3076236447 .md ol,
.d2-3076236447 .md dl,
.d2-3076236447 .md table,
.d2-3076236447 .md pre,
.d2-3076236447 .md details {
margin-top: 0;
margin-bottom: 16px;
}
.d2-3076236447 .md blockquote > :first-child {
margin-top: 0;
}
.d2-3076236447 .md blockquote > :last-child {
margin-bottom: 0;
}
.d2-3076236447 .md sup > a::before {
content: "[";
}
.d2-3076236447 .md sup > a::after {
content: "]";
}
.d2-3076236447 .md h1:hover .anchor,
.d2-3076236447 .md h2:hover .anchor,
.d2-3076236447 .md h3:hover .anchor,
.d2-3076236447 .md h4:hover .anchor,
.d2-3076236447 .md h5:hover .anchor,
.d2-3076236447 .md h6:hover .anchor {
text-decoration: none;
}
.d2-3076236447 .md h1 tt,
.d2-3076236447 .md h1 code,
.d2-3076236447 .md h2 tt,
.d2-3076236447 .md h2 code,
.d2-3076236447 .md h3 tt,
.d2-3076236447 .md h3 code,
.d2-3076236447 .md h4 tt,
.d2-3076236447 .md h4 code,
.d2-3076236447 .md h5 tt,
.d2-3076236447 .md h5 code,
.d2-3076236447 .md h6 tt,
.d2-3076236447 .md h6 code {
padding: 0 0.2em;
font-size: inherit;
}
.d2-3076236447 .md ul.no-list,
.d2-3076236447 .md ol.no-list {
padding: 0;
list-style-type: none;
}
.d2-3076236447 .md ol[type="1"] {
list-style-type: decimal;
}
.d2-3076236447 .md ol[type="a"] {
list-style-type: lower-alpha;
}
.d2-3076236447 .md ol[type="i"] {
list-style-type: lower-roman;
}
.d2-3076236447 .md div > ol:not([type]) {
list-style-type: decimal;
}
.d2-3076236447 .md ul ul,
.d2-3076236447 .md ul ol,
.d2-3076236447 .md ol ol,
.d2-3076236447 .md ol ul {
margin-top: 0;
margin-bottom: 0;
}
.d2-3076236447 .md li > p {
margin-top: 16px;
}
.d2-3076236447 .md li + li {
margin-top: 0.25em;
}
.d2-3076236447 .md dl {
padding: 0;
}
.d2-3076236447 .md dl dt {
padding: 0;
margin-top: 16px;
font-size: 1em;
font-style: italic;
font-family: "d2-3076236447-font-semibold";
}
.d2-3076236447 .md dl dd {
padding: 0 16px;
margin-bottom: 16px;
}
.d2-3076236447 .md table th {
font-family: "d2-3076236447-font-semibold";
}
.d2-3076236447 .md table th,
.d2-3076236447 .md table td {
padding: 6px 13px;
border: 1px solid var(--color-border-default);
}
.d2-3076236447 .md table tr {
background-color: var(--color-canvas-default);
border-top: 1px solid var(--color-border-muted);
}
.d2-3076236447 .md table tr:nth-child(2n) {
background-color: var(--color-canvas-subtle);
}
.d2-3076236447 .md table img {
background-color: transparent;
}
.d2-3076236447 .md img[align="right"] {
padding-left: 20px;
}
.d2-3076236447 .md img[align="left"] {
padding-right: 20px;
}
.d2-3076236447 .md span.frame {
display: block;
overflow: hidden;
}
.d2-3076236447 .md span.frame > span {
display: block;
float: left;
width: auto;
padding: 7px;
margin: 13px 0 0;
overflow: hidden;
border: 1px solid var(--color-border-default);
}
.d2-3076236447 .md span.frame span img {
display: block;
float: left;
}
.d2-3076236447 .md span.frame span span {
display: block;
padding: 5px 0 0;
clear: both;
color: var(--color-fg-default);
}
.d2-3076236447 .md span.align-center {
display: block;
overflow: hidden;
clear: both;
}
.d2-3076236447 .md span.align-center > span {
display: block;
margin: 13px auto 0;
overflow: hidden;
text-align: center;
}
.d2-3076236447 .md span.align-center span img {
margin: 0 auto;
text-align: center;
}
.d2-3076236447 .md span.align-right {
display: block;
overflow: hidden;
clear: both;
}
.d2-3076236447 .md span.align-right > span {
display: block;
margin: 13px 0 0;
overflow: hidden;
text-align: right;
}
.d2-3076236447 .md span.align-right span img {
margin: 0;
text-align: right;
}
.d2-3076236447 .md span.float-left {
display: block;
float: left;
margin-right: 13px;
overflow: hidden;
}
.d2-3076236447 .md span.float-left span {
margin: 13px 0 0;
}
.d2-3076236447 .md span.float-right {
display: block;
float: right;
margin-left: 13px;
overflow: hidden;
}
.d2-3076236447 .md span.float-right > span {
display: block;
margin: 13px auto 0;
overflow: hidden;
text-align: right;
}
.d2-3076236447 .md code,
.d2-3076236447 .md tt {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
background-color: var(--color-neutral-muted);
border-radius: 6px;
}
.d2-3076236447 .md code br,
.d2-3076236447 .md tt br {
display: none;
}
.d2-3076236447 .md del code {
text-decoration: inherit;
}
.d2-3076236447 .md pre code {
font-size: 100%;
}
.d2-3076236447 .md pre > code {
padding: 0;
margin: 0;
word-break: normal;
white-space: pre;
background: transparent;
border: 0;
}
.d2-3076236447 .md .highlight {
margin-bottom: 16px;
}
.d2-3076236447 .md .highlight pre {
margin-bottom: 0;
word-break: normal;
}
.d2-3076236447 .md .highlight pre,
.d2-3076236447 .md pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: var(--color-canvas-subtle);
border-radius: 6px;
}
.d2-3076236447 .md pre code,
.d2-3076236447 .md pre tt {
display: inline;
max-width: auto;
padding: 0;
margin: 0;
overflow: visible;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
border: 0;
}
.d2-3076236447 .md .csv-data td,
.d2-3076236447 .md .csv-data th {
padding: 5px;
overflow: hidden;
font-size: 12px;
line-height: 1;
text-align: left;
white-space: nowrap;
}
.d2-3076236447 .md .csv-data .blob-num {
padding: 10px 8px 9px;
text-align: right;
background: var(--color-canvas-default);
border: 0;
}
.d2-3076236447 .md .csv-data tr {
border-top: 0;
}
.d2-3076236447 .md .csv-data th {
font-family: "d2-3076236447-font-semibold";
background: var(--color-canvas-subtle);
border-top: 0;
}
.d2-3076236447 .md .footnotes {
font-size: 12px;
color: var(--color-fg-muted);
border-top: 1px solid var(--color-border-default);
}
.d2-3076236447 .md .footnotes ol {
padding-left: 16px;
}
.d2-3076236447 .md .footnotes li {
position: relative;
}
.d2-3076236447 .md .footnotes li:target::before {
position: absolute;
top: -8px;
right: -8px;
bottom: -8px;
left: -24px;
pointer-events: none;
content: "";
border: 2px solid var(--color-accent-emphasis);
border-radius: 6px;
}
.d2-3076236447 .md .footnotes li:target {
color: var(--color-fg-default);
}
.d2-3076236447 .md .task-list-item {
list-style-type: none;
}
.d2-3076236447 .md .task-list-item label {
font-weight: 400;
}
.d2-3076236447 .md .task-list-item.enabled label {
cursor: pointer;
}
.d2-3076236447 .md .task-list-item + .task-list-item {
margin-top: 3px;
}
.d2-3076236447 .md .task-list-item .handle {
display: none;
}
.d2-3076236447 .md .task-list-item-checkbox {
margin: 0 0.2em 0.25em -1.6em;
vertical-align: middle;
}
.d2-3076236447 .md .contains-task-list:dir(rtl) .task-list-item-checkbox {
margin: 0 -1.6em 0.25em 0.2em;
}
</style><style type="text/css"><![CDATA[
.dots-overlay {
fill: url(#dots-d2-3076236447);
mix-blend-mode: multiply;
}
@media (prefers-color-scheme: dark) {
.d2-3076236447 .fill-N1 { fill: #E8E8E8; }
.d2-3076236447 .fill-N2 { fill: #CCCCCC; }
.d2-3076236447 .fill-N3 { fill: #999999; }
.d2-3076236447 .fill-B1 { fill: #E8E8E8; }
.d2-3076236447 .fill-B2 { fill: #7BACFF; }
.d2-3076236447 .fill-B3 { fill: #6AAFDC; }
.d2-3076236447 .fill-N4 { fill: #3A3A44; }
.d2-3076236447 .fill-N5 { fill: #2E2E38; }
.d2-3076236447 .fill-N6 { fill: #252530; }
.d2-3076236447 .fill-N7 { fill: transparent; }
.d2-3076236447 .fill-B4 { fill: #2A2A34; }
.d2-3076236447 .fill-B5 { fill: #1E1E28; }
.d2-3076236447 .fill-B6 { fill: #16161E; }
.d2-3076236447 .stroke-N1 { stroke: #E8E8E8; }
.d2-3076236447 .stroke-N2 { stroke: #CCCCCC; }
.d2-3076236447 .stroke-N3 { stroke: #999999; }
.d2-3076236447 .stroke-N4 { stroke: #555555; }
.d2-3076236447 .stroke-N5 { stroke: #444444; }
.d2-3076236447 .stroke-N6 { stroke: #333333; }
.d2-3076236447 .stroke-N7 { stroke: transparent; }
.d2-3076236447 .stroke-B1 { stroke: #E8E8E8; }
.d2-3076236447 .stroke-B2 { stroke: #7BACFF; }
.d2-3076236447 .stroke-B3 { stroke: #6AAFDC; }
.d2-3076236447 .stroke-B4 { stroke: #3A3A44; }
.d2-3076236447 .stroke-B5 { stroke: #2E2E38; }
.d2-3076236447 .stroke-B6 { stroke: #252530; }
.d2-3076236447 .color-N1 { color: #E8E8E8; }
.d2-3076236447 .color-N2 { color: #CCCCCC; }
.d2-3076236447 .color-N3 { color: #999999; }
.d2-3076236447 .connection.fill-B1 { fill: #E8E8E8; }
}
]]></style><defs><pattern id="dots-d2-3076236447" x="0" y="0" width="15" height="15" patternUnits="userSpaceOnUse">
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="2" y="2" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="12" y="2" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="12" y="12" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="2" y="12" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="2" y="7" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="12" y="7" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="7" y="2" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="7" y="12" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="7" y="7" width="1" height="1" fill="#0A0F25"/>
</g>
</pattern>
</defs><g class="YWdncmVnYXRlZA=="><g class="shape" ><rect x="246.000000" y="12.000000" width="1136.000000" height="450.000000" stroke="#000410" fill="#E7E9EE" class=" stroke-B1 fill-B4" style="stroke-width:2;" /><rect x="246.000000" y="12.000000" width="1136.000000" height="450.000000" class="dots-overlay" style="stroke-width:2;" /><rect x="251.000000" y="17.000000" width="1126.000000" height="440.000000" stroke="#000410" fill="transparent" class=" stroke-B1" style="stroke-width:2;" /></g><text x="814.000000" y="41.000000" fill="#000410" class="text-mono fill-N1" style="text-anchor:middle;font-size:24px">AGGREGATED</text></g><g class="ZGlzYWdncmVnYXRlZA=="><g class="shape" ><rect x="12.000000" y="482.000000" width="1605.000000" height="450.000000" stroke="#000410" fill="#E7E9EE" class=" stroke-B1 fill-B4" style="stroke-width:2;" /><rect x="12.000000" y="482.000000" width="1605.000000" height="450.000000" class="dots-overlay" style="stroke-width:2;" /><rect x="17.000000" y="487.000000" width="1595.000000" height="440.000000" stroke="#000410" fill="transparent" class=" stroke-B1" style="stroke-width:2;" /></g><text x="814.500000" y="511.000000" fill="#000410" class="text-mono fill-N1" style="text-anchor:middle;font-size:24px">DISAGGREGATED</text></g><g class="YWdncmVnYXRlZC5mcm9udGVuZA=="><g class="shape" ><rect x="592.000000" y="207.000000" width="180.000000" height="60.000000" stroke="#000410" fill="#F5F6F9" class=" stroke-B1 fill-B5" style="stroke-width:2;" /></g><text x="682.000000" y="244.000000" fill="#000410" class="text-mono-bold fill-N1" style="text-anchor:middle;font-size:20px">FRONTEND</text></g><g class="YWdncmVnYXRlZC5yb3V0ZXI="><g class="shape" ><rect x="842.000000" y="207.000000" width="180.000000" height="60.000000" stroke="#000410" fill="#F5F6F9" class=" stroke-B1 fill-B5" style="stroke-width:2;" /></g><text x="932.000000" y="244.000000" fill="#000410" class="text-mono-bold fill-N1" style="text-anchor:middle;font-size:20px">ROUTER</text></g><g class="YWdncmVnYXRlZC53MQ=="><g class="shape" ><rect x="1152.000000" y="87.000000" width="180.000000" height="60.000000" stroke="#000410" fill="#F5F6F9" class=" stroke-B1 fill-B5" style="stroke-width:2;" /></g><text x="1242.000000" y="124.000000" fill="#000410" class="text-mono-bold fill-N1" style="text-anchor:middle;font-size:20px">W1 (TP2)</text></g><g class="YWdncmVnYXRlZC53Mg=="><g class="shape" ><rect x="1152.000000" y="167.000000" width="180.000000" height="60.000000" stroke="#000410" fill="#F5F6F9" class=" stroke-B1 fill-B5" style="stroke-width:2;" /></g><text x="1242.000000" y="204.000000" fill="#000410" class="text-mono-bold fill-N1" style="text-anchor:middle;font-size:20px">W2 (TP2)</text></g><g class="YWdncmVnYXRlZC53Mw=="><g class="shape" ><rect x="1152.000000" y="247.000000" width="180.000000" height="60.000000" stroke="#000410" fill="#F5F6F9" class=" stroke-B1 fill-B5" style="stroke-width:2;" /></g><text x="1242.000000" y="284.000000" fill="#000410" class="text-mono-bold fill-N1" style="text-anchor:middle;font-size:20px">W3 (TP2)</text></g><g class="YWdncmVnYXRlZC53NA=="><g class="shape" ><rect x="1152.000000" y="327.000000" width="180.000000" height="60.000000" stroke="#000410" fill="#F5F6F9" class=" stroke-B1 fill-B5" style="stroke-width:2;" /></g><text x="1242.000000" y="364.000000" fill="#000410" class="text-mono-bold fill-N1" style="text-anchor:middle;font-size:20px">W4 (TP2)</text></g><g class="YWdncmVnYXRlZC5ub3Rl"><g class="shape" ></g><g><foreignObject requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" x="296.000000" y="287.000000" width="476" height="27"><div xmlns="http://www.w3.org/1999/xhtml" class="md color-N1" style="font-size:18px"><p>EACH WORKER HANDLES BOTH PREFILL AND DECODE.</p>
</div></foreignObject></g></g><g class="ZGlzYWdncmVnYXRlZC5mcm9udGVuZA=="><g class="shape" ><rect x="304.000000" y="673.000000" width="180.000000" height="60.000000" stroke="#000410" fill="#F5F6F9" class=" stroke-B1 fill-B5" style="stroke-width:2;" /></g><text x="394.000000" y="710.000000" fill="#000410" class="text-mono-bold fill-N1" style="text-anchor:middle;font-size:20px">FRONTEND</text></g><g class="ZGlzYWdncmVnYXRlZC5yb3V0ZXI="><g class="shape" ><rect x="554.000000" y="673.000000" width="180.000000" height="60.000000" stroke="#000410" fill="#F5F6F9" class=" stroke-B1 fill-B5" style="stroke-width:2;" /></g><text x="644.000000" y="710.000000" fill="#000410" class="text-mono-bold fill-N1" style="text-anchor:middle;font-size:20px">ROUTER</text></g><g class="ZGlzYWdncmVnYXRlZC5wMQ=="><g class="shape" ><rect x="814.000000" y="633.000000" width="220.000000" height="60.000000" stroke="#000410" fill="#F5F6F9" class=" stroke-B1 fill-B5" style="stroke-width:2;" /></g><text x="924.000000" y="670.000000" fill="#000410" class="text-mono-bold fill-N1" style="text-anchor:middle;font-size:20px">PREFILL 1 (TP2)</text></g><g class="ZGlzYWdncmVnYXRlZC5wMg=="><g class="shape" ><rect x="814.000000" y="713.000000" width="220.000000" height="60.000000" stroke="#000410" fill="#F5F6F9" class=" stroke-B1 fill-B5" style="stroke-width:2;" /></g><text x="924.000000" y="750.000000" fill="#000410" class="text-mono-bold fill-N1" style="text-anchor:middle;font-size:20px">PREFILL 2 (TP2)</text></g><g class="ZGlzYWdncmVnYXRlZC5kZWNvZGU="><g class="shape" ><rect x="1347.000000" y="673.000000" width="220.000000" height="60.000000" stroke="#000410" fill="#F5F6F9" class=" stroke-B1 fill-B5" style="stroke-width:2;" /></g><text x="1457.000000" y="710.000000" fill="#000410" class="text-mono-bold fill-N1" style="text-anchor:middle;font-size:20px">DECODE (TP4)</text></g><g class="ZGlzYWdncmVnYXRlZC5ub3Rl"><g class="shape" ></g><g><foreignObject requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" x="62.000000" y="753.000000" width="422" height="27"><div xmlns="http://www.w3.org/1999/xhtml" class="md color-N1" style="font-size:18px"><p>PREFILL AND DECODE ON SEPARATE WORKERS.</p>
</div></foreignObject></g></g><g class="YWdncmVnYXRlZC4oZnJvbnRlbmQgLSZndDsgcm91dGVyKVswXQ=="><marker id="mk-d2-3076236447-3488378134" markerWidth="10.000000" markerHeight="12.000000" refX="7.000000" refY="6.000000" viewBox="0.000000 0.000000 10.000000 12.000000" orient="auto" markerUnits="userSpaceOnUse"> <polygon points="0.000000,0.000000 10.000000,6.000000 0.000000,12.000000" fill="#000410" class="connection fill-B1" stroke-width="2" /> </marker><path d="M 774.500000 237.000000 L 838.500000 237.000000" stroke="#000410" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-d2-3076236447-3488378134)" mask="url(#d2-3076236447)" /></g><g class="YWdncmVnYXRlZC4ocm91dGVyIC0mZ3Q7IHcxKVswXQ=="><path d="M 1024.500000 219.000000 L 1062.500000 219.000000 S 1062.500000 219.000000 1062.500000 219.000000 L 1062.500000 117.000000 S 1062.500000 117.000000 1062.500000 117.000000 L 1148.500000 117.000000" stroke="#000410" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-d2-3076236447-3488378134)" mask="url(#d2-3076236447)" /></g><g class="YWdncmVnYXRlZC4ocm91dGVyIC0mZ3Q7IHcyKVswXQ=="><path d="M 1024.500000 231.000000 L 1112.500000 231.000000 S 1112.500000 231.000000 1112.500000 231.000000 L 1112.500000 197.000000 S 1112.500000 197.000000 1112.500000 197.000000 L 1148.500000 197.000000" stroke="#000410" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-d2-3076236447-3488378134)" mask="url(#d2-3076236447)" /></g><g class="YWdncmVnYXRlZC4ocm91dGVyIC0mZ3Q7IHczKVswXQ=="><path d="M 1024.500000 243.000000 L 1112.500000 243.000000 S 1112.500000 243.000000 1112.500000 243.000000 L 1112.500000 277.000000 S 1112.500000 277.000000 1112.500000 277.000000 L 1148.500000 277.000000" stroke="#000410" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-d2-3076236447-3488378134)" mask="url(#d2-3076236447)" /></g><g class="YWdncmVnYXRlZC4ocm91dGVyIC0mZ3Q7IHc0KVswXQ=="><path d="M 1024.500000 255.000000 L 1062.500000 255.000000 S 1062.500000 255.000000 1062.500000 255.000000 L 1062.500000 357.000000 S 1062.500000 357.000000 1062.500000 357.000000 L 1148.500000 357.000000" stroke="#000410" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-d2-3076236447-3488378134)" mask="url(#d2-3076236447)" /></g><g class="ZGlzYWdncmVnYXRlZC4oZnJvbnRlbmQgLSZndDsgcm91dGVyKVswXQ=="><path d="M 486.000000 703.500000 L 550.000000 703.500000" stroke="#000410" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-d2-3076236447-3488378134)" mask="url(#d2-3076236447)" /></g><g class="ZGlzYWdncmVnYXRlZC4ocm91dGVyIC0mZ3Q7IHAxKVswXQ=="><path d="M 736.000000 693.500000 L 774.000000 693.500000 S 774.000000 693.500000 774.000000 693.500000 L 774.000000 663.500000 S 774.000000 663.500000 774.000000 663.500000 L 810.000000 663.500000" stroke="#000410" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-d2-3076236447-3488378134)" mask="url(#d2-3076236447)" /></g><g class="ZGlzYWdncmVnYXRlZC4ocm91dGVyIC0mZ3Q7IHAyKVswXQ=="><path d="M 736.000000 713.500000 L 774.000000 713.500000 S 774.000000 713.500000 774.000000 713.500000 L 774.000000 743.500000 S 774.000000 743.500000 774.000000 743.500000 L 810.000000 743.500000" stroke="#000410" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-d2-3076236447-3488378134)" mask="url(#d2-3076236447)" /></g><g class="ZGlzYWdncmVnYXRlZC4ocDEgLSZndDsgZGVjb2RlKVswXQ=="><path d="M 1036.000000 663.500000 L 1307.000000 663.500000 S 1307.000000 663.500000 1307.000000 663.500000 L 1307.000000 693.500000 S 1307.000000 693.500000 1307.000000 693.500000 L 1343.000000 693.500000" stroke="#000410" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-d2-3076236447-3488378134)" mask="url(#d2-3076236447)" /><text x="1205.500000" y="669.000000" fill="#0000B8" class="text-mono-italic fill-N2" style="text-anchor:middle;font-size:16px">KV CACHE VIA RDMA</text></g><g class="ZGlzYWdncmVnYXRlZC4ocDIgLSZndDsgZGVjb2RlKVswXQ=="><path d="M 1036.000000 743.500000 L 1307.000000 743.500000 S 1307.000000 743.500000 1307.000000 743.500000 L 1307.000000 713.500000 S 1307.000000 713.500000 1307.000000 713.500000 L 1343.000000 713.500000" stroke="#000410" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-d2-3076236447-3488378134)" mask="url(#d2-3076236447)" /><text x="1205.500000" y="749.000000" fill="#0000B8" class="text-mono-italic fill-N2" style="text-anchor:middle;font-size:16px">KV CACHE VIA RDMA</text></g><mask id="d2-3076236447" maskUnits="userSpaceOnUse" x="-19" y="-19" width="1667" height="982">
<rect x="-19" y="-19" width="1667" height="982" fill="white"></rect>
<rect x="1122.000000" y="653.000000" width="167" height="21" fill="black"></rect>
<rect x="1122.000000" y="733.000000" width="167" height="21" fill="black"></rect>
</mask></svg></svg>
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" data-d2-version="0.7.1" preserveAspectRatio="xMinYMin meet" viewBox="0 0 821 1160"><svg class="d2-1215148716 d2-svg" width="821" height="1160" viewBox="-9 -9 821 1160"><rect x="-9.000000" y="-9.000000" width="821.000000" height="1160.000000" rx="0.000000" fill="transparent" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-1215148716 .text-mono {
font-family: "d2-1215148716-font-mono";
}
@font-face {
font-family: d2-1215148716-font-mono;
src: url("data:application/font-woff;base64,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");
}
.d2-1215148716 .text-mono-bold {
font-family: "d2-1215148716-font-mono-bold";
}
@font-face {
font-family: d2-1215148716-font-mono-bold;
src: url("data:application/font-woff;base64,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");
}
.d2-1215148716 .text-mono-italic {
font-family: "d2-1215148716-font-mono-italic";
}
@font-face {
font-family: d2-1215148716-font-mono-italic;
src: url("data:application/font-woff;base64,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");
}
@media (prefers-color-scheme: dark) {
.d2-1215148716 .fill-N1 { fill: #E8E8E8; }
.d2-1215148716 .fill-N2 { fill: #CCCCCC; }
.d2-1215148716 .fill-N3 { fill: #999999; }
.d2-1215148716 .fill-B1 { fill: #E8E8E8; }
.d2-1215148716 .fill-N4 { fill: #3A3A44; }
.d2-1215148716 .fill-N5 { fill: #2E2E38; }
.d2-1215148716 .fill-N6 { fill: #252530; }
.d2-1215148716 .fill-N7 { fill: transparent; }
.d2-1215148716 .fill-B4 { fill: #2A2A34; }
.d2-1215148716 .fill-B5 { fill: #1E1E28; }
.d2-1215148716 .fill-B6 { fill: #16161E; }
.d2-1215148716 .stroke-N1 { stroke: #E8E8E8; }
.d2-1215148716 .stroke-N2 { stroke: #CCCCCC; }
.d2-1215148716 .stroke-N3 { stroke: #999999; }
.d2-1215148716 .stroke-N4 { stroke: #555555; }
.d2-1215148716 .stroke-N5 { stroke: #444444; }
.d2-1215148716 .stroke-N6 { stroke: #333333; }
.d2-1215148716 .stroke-N7 { stroke: transparent; }
.d2-1215148716 .stroke-B1 { stroke: #E8E8E8; }
.d2-1215148716 .stroke-B4 { stroke: #3A3A44; }
.d2-1215148716 .stroke-B5 { stroke: #2E2E38; }
.d2-1215148716 .stroke-B6 { stroke: #252530; }
.d2-1215148716 .color-N1 { color: #E8E8E8; }
.d2-1215148716 .color-N2 { color: #CCCCCC; }
.d2-1215148716 .color-N3 { color: #999999; }
.d2-1215148716 .connection.fill-B1 { fill: #E8E8E8; }
}
]]></style><style type="text/css"><![CDATA[.shape {
shape-rendering: geometricPrecision;
stroke-linejoin: round;
}
.connection {
stroke-linecap: round;
stroke-linejoin: round;
}
.blend {
mix-blend-mode: multiply;
opacity: 0.5;
}
.d2-1215148716 .fill-N1{fill:#000410;}
.d2-1215148716 .fill-N2{fill:#0000B8;}
.d2-1215148716 .fill-N3{fill:#9499AB;}
.d2-1215148716 .fill-N4{fill:#CFD2DD;}
.d2-1215148716 .fill-N5{fill:#C3DEF3;}
.d2-1215148716 .fill-N6{fill:#EEF1F8;}
.d2-1215148716 .fill-N7{fill:#FFFFFF;}
.d2-1215148716 .fill-B1{fill:#000410;}
.d2-1215148716 .fill-B2{fill:#0000E4;}
.d2-1215148716 .fill-B3{fill:#5AA4DC;}
.d2-1215148716 .fill-B4{fill:#E7E9EE;}
.d2-1215148716 .fill-B5{fill:#F5F6F9;}
.d2-1215148716 .fill-B6{fill:#FFFFFF;}
.d2-1215148716 .fill-AA2{fill:#008566;}
.d2-1215148716 .fill-AA4{fill:#45BBA5;}
.d2-1215148716 .fill-AA5{fill:#7ACCBD;}
.d2-1215148716 .fill-AB4{fill:#F1C759;}
.d2-1215148716 .fill-AB5{fill:#F9E088;}
.d2-1215148716 .stroke-N1{stroke:#000410;}
.d2-1215148716 .stroke-N2{stroke:#0000B8;}
.d2-1215148716 .stroke-N3{stroke:#9499AB;}
.d2-1215148716 .stroke-N4{stroke:#CFD2DD;}
.d2-1215148716 .stroke-N5{stroke:#C3DEF3;}
.d2-1215148716 .stroke-N6{stroke:#EEF1F8;}
.d2-1215148716 .stroke-N7{stroke:#FFFFFF;}
.d2-1215148716 .stroke-B1{stroke:#000410;}
.d2-1215148716 .stroke-B2{stroke:#0000E4;}
.d2-1215148716 .stroke-B3{stroke:#5AA4DC;}
.d2-1215148716 .stroke-B4{stroke:#E7E9EE;}
.d2-1215148716 .stroke-B5{stroke:#F5F6F9;}
.d2-1215148716 .stroke-B6{stroke:#FFFFFF;}
.d2-1215148716 .stroke-AA2{stroke:#008566;}
.d2-1215148716 .stroke-AA4{stroke:#45BBA5;}
.d2-1215148716 .stroke-AA5{stroke:#7ACCBD;}
.d2-1215148716 .stroke-AB4{stroke:#F1C759;}
.d2-1215148716 .stroke-AB5{stroke:#F9E088;}
.d2-1215148716 .background-color-N1{background-color:#000410;}
.d2-1215148716 .background-color-N2{background-color:#0000B8;}
.d2-1215148716 .background-color-N3{background-color:#9499AB;}
.d2-1215148716 .background-color-N4{background-color:#CFD2DD;}
.d2-1215148716 .background-color-N5{background-color:#C3DEF3;}
.d2-1215148716 .background-color-N6{background-color:#EEF1F8;}
.d2-1215148716 .background-color-N7{background-color:#FFFFFF;}
.d2-1215148716 .background-color-B1{background-color:#000410;}
.d2-1215148716 .background-color-B2{background-color:#0000E4;}
.d2-1215148716 .background-color-B3{background-color:#5AA4DC;}
.d2-1215148716 .background-color-B4{background-color:#E7E9EE;}
.d2-1215148716 .background-color-B5{background-color:#F5F6F9;}
.d2-1215148716 .background-color-B6{background-color:#FFFFFF;}
.d2-1215148716 .background-color-AA2{background-color:#008566;}
.d2-1215148716 .background-color-AA4{background-color:#45BBA5;}
.d2-1215148716 .background-color-AA5{background-color:#7ACCBD;}
.d2-1215148716 .background-color-AB4{background-color:#F1C759;}
.d2-1215148716 .background-color-AB5{background-color:#F9E088;}
.d2-1215148716 .color-N1{color:#000410;}
.d2-1215148716 .color-N2{color:#0000B8;}
.d2-1215148716 .color-N3{color:#9499AB;}
.d2-1215148716 .color-N4{color:#CFD2DD;}
.d2-1215148716 .color-N5{color:#C3DEF3;}
.d2-1215148716 .color-N6{color:#EEF1F8;}
.d2-1215148716 .color-N7{color:#FFFFFF;}
.d2-1215148716 .color-B1{color:#000410;}
.d2-1215148716 .color-B2{color:#0000E4;}
.d2-1215148716 .color-B3{color:#5AA4DC;}
.d2-1215148716 .color-B4{color:#E7E9EE;}
.d2-1215148716 .color-B5{color:#F5F6F9;}
.d2-1215148716 .color-B6{color:#FFFFFF;}
.d2-1215148716 .color-AA2{color:#008566;}
.d2-1215148716 .color-AA4{color:#45BBA5;}
.d2-1215148716 .color-AA5{color:#7ACCBD;}
.d2-1215148716 .color-AB4{color:#F1C759;}
.d2-1215148716 .color-AB5{color:#F9E088;}.appendix text.text{fill:#000410}.md{--color-fg-default:#000410;--color-fg-muted:#0000B8;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#000410;--color-border-muted:#0000E4;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0000E4;--color-accent-emphasis:#0000E4;--color-attention-subtle:#0000B8;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker-d2-1215148716);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker-d2-1215148716);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-normal-d2-1215148716);mix-blend-mode:color-burn}.sketch-overlay-B4{fill:url(#streaks-bright-d2-1215148716);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright-d2-1215148716);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright-d2-1215148716);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark-d2-1215148716);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-normal-d2-1215148716);mix-blend-mode:color-burn}.sketch-overlay-AA5{fill:url(#streaks-normal-d2-1215148716);mix-blend-mode:color-burn}.sketch-overlay-AB4{fill:url(#streaks-normal-d2-1215148716);mix-blend-mode:color-burn}.sketch-overlay-AB5{fill:url(#streaks-normal-d2-1215148716);mix-blend-mode:color-burn}.sketch-overlay-N1{fill:url(#streaks-darker-d2-1215148716);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-darker-d2-1215148716);mix-blend-mode:lighten}.sketch-overlay-N3{fill:url(#streaks-normal-d2-1215148716);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal-d2-1215148716);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-normal-d2-1215148716);mix-blend-mode:color-burn}.sketch-overlay-N6{fill:url(#streaks-bright-d2-1215148716);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright-d2-1215148716);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}
@media (prefers-color-scheme: dark) {
.d2-1215148716 .fill-N1 { fill: #E8E8E8; }
.d2-1215148716 .fill-N2 { fill: #CCCCCC; }
.d2-1215148716 .fill-N3 { fill: #999999; }
.d2-1215148716 .fill-B1 { fill: #E8E8E8; }
.d2-1215148716 .fill-N4 { fill: #3A3A44; }
.d2-1215148716 .fill-N5 { fill: #2E2E38; }
.d2-1215148716 .fill-N6 { fill: #252530; }
.d2-1215148716 .fill-N7 { fill: transparent; }
.d2-1215148716 .fill-B4 { fill: #2A2A34; }
.d2-1215148716 .fill-B5 { fill: #1E1E28; }
.d2-1215148716 .fill-B6 { fill: #16161E; }
.d2-1215148716 .stroke-N1 { stroke: #E8E8E8; }
.d2-1215148716 .stroke-N2 { stroke: #CCCCCC; }
.d2-1215148716 .stroke-N3 { stroke: #999999; }
.d2-1215148716 .stroke-N4 { stroke: #555555; }
.d2-1215148716 .stroke-N5 { stroke: #444444; }
.d2-1215148716 .stroke-N6 { stroke: #333333; }
.d2-1215148716 .stroke-N7 { stroke: transparent; }
.d2-1215148716 .stroke-B1 { stroke: #E8E8E8; }
.d2-1215148716 .stroke-B4 { stroke: #3A3A44; }
.d2-1215148716 .stroke-B5 { stroke: #2E2E38; }
.d2-1215148716 .stroke-B6 { stroke: #252530; }
.d2-1215148716 .color-N1 { color: #E8E8E8; }
.d2-1215148716 .color-N2 { color: #CCCCCC; }
.d2-1215148716 .color-N3 { color: #999999; }
.d2-1215148716 .connection.fill-B1 { fill: #E8E8E8; }
}
]]></style><g class="cTE="><g class="shape" ><path d="M 476 136 C 474 136 473 136 472 136 L 252 75 C 250 74 250 73 252 73 L 472 12 C 474 11 478 11 480 12 L 700 72 C 702 73 702 74 700 74 L 480 136 C 479 136 478 136 476 136 Z" stroke="#000410" fill="#CFD2DD" class=" stroke-B1 fill-N4" style="stroke-width:2;" /></g><text x="476.000000" y="71.500000" fill="#000410" class="text-mono-bold fill-N1" style="text-anchor:middle;font-size:16px"><tspan x="476.000000" dy="0.000000">AIC SHOWS DISAGG &gt; AGG</tspan><tspan x="476.000000" dy="18.500000">THROUGHPUT?</tspan></text></g><g class="cTI="><g class="shape" ><path d="M 382 391 C 381 391 380 391 379 391 L 235 330 C 233 329 233 328 235 328 L 379 267 C 381 266 383 266 384 267 L 528 327 C 530 328 530 329 528 329 L 385 391 C 384 391 383 391 382 391 Z" stroke="#000410" fill="#CFD2DD" class=" stroke-B1 fill-N4" style="stroke-width:2;" /></g><text x="382.000000" y="326.500000" fill="#000410" class="text-mono-bold fill-N1" style="text-anchor:middle;font-size:16px"><tspan x="382.000000" dy="0.000000">RDMA AVAILABLE</tspan><tspan x="382.000000" dy="18.500000">IN CLUSTER?</tspan></text></g><g class="cTM="><g class="shape" ><path d="M 260 614 C 258 614 257 614 256 614 L 57 569 C 55 569 55 568 57 567 L 256 522 C 258 522 261 522 263 522 L 463 567 C 465 567 465 568 463 569 L 264 614 C 263 614 262 614 260 614 Z" stroke="#000410" fill="#CFD2DD" class=" stroke-B1 fill-N4" style="stroke-width:2;" /></g><text x="260.000000" y="573.500000" fill="#000410" class="text-mono-bold fill-N1" style="text-anchor:middle;font-size:16px">ISL/OSL RATIO &gt; 8:1?</text></g><g class="cTQ="><g class="shape" ><path d="M 345 869 C 344 869 343 869 343 869 L 216 808 C 215 807 215 806 216 806 L 343 745 C 344 744 346 744 348 745 L 475 805 C 476 806 476 807 475 807 L 347 869 C 347 869 346 869 345 869 Z" stroke="#000410" fill="#CFD2DD" class=" stroke-B1 fill-N4" style="stroke-width:2;" /></g><text x="345.000000" y="804.500000" fill="#000410" class="text-mono-bold fill-N1" style="text-anchor:middle;font-size:16px"><tspan x="345.000000" dy="0.000000">DISAGG &gt; 20%</tspan><tspan x="345.000000" dy="18.500000">FASTER?</tspan></text></g><g class="YWdn"><g class="shape" ><rect x="411.000000" y="1050.000000" width="380.000000" height="80.000000" stroke="#000410" fill="#FFFFFF" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="601.000000" y="1087.500000" fill="#000410" class="text-mono-bold fill-N1" style="text-anchor:middle;font-size:16px"><tspan x="601.000000" dy="0.000000">AGGREGATED</tspan><tspan x="601.000000" dy="18.500000">SIMPLER, NO RDMA NEEDED</tspan></text></g><g class="ZGlzYWdn"><g class="shape" ><rect x="12.000000" y="1050.000000" width="380.000000" height="80.000000" stroke="#000410" fill="#FFFFFF" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="202.000000" y="1087.500000" fill="#000410" class="text-mono-bold fill-N1" style="text-anchor:middle;font-size:16px"><tspan x="202.000000" dy="0.000000">DISAGGREGATED</tspan><tspan x="202.000000" dy="18.500000">HIGHER THROUGHPUT, NEEDS RDMA</tspan></text></g><g class="KHExIC0mZ3Q7IHEyKVswXQ=="><marker id="mk-d2-1215148716-3488378134" markerWidth="10.000000" markerHeight="12.000000" refX="7.000000" refY="6.000000" viewBox="0.000000 0.000000 10.000000 12.000000" orient="auto" markerUnits="userSpaceOnUse"> <polygon points="0.000000,0.000000 10.000000,6.000000 0.000000,12.000000" fill="#000410" class="connection fill-B1" stroke-width="2" /> </marker><path d="M 383.000000 113.000000 L 383.000000 262.000000" stroke="#000410" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-d2-1215148716-3488378134)" mask="url(#d2-1215148716)" /><text x="383.000000" y="194.000000" fill="#0000B8" class="text-mono-italic fill-N2" style="text-anchor:middle;font-size:16px">YES</text></g><g class="KHExIC0mZ3Q7IGFnZylbMF0="><path d="M 675.000000 83.000000 L 675.000000 1046.000000" stroke="#000410" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-d2-1215148716-3488378134)" mask="url(#d2-1215148716)" /><text x="675.000000" y="571.000000" fill="#0000B8" class="text-mono-italic fill-N2" style="text-anchor:middle;font-size:16px">NO</text></g><g class="KHEyIC0mZ3Q7IHEzKVswXQ=="><path d="M 260.000000 342.000000 L 260.000000 518.000000" stroke="#000410" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-d2-1215148716-3488378134)" mask="url(#d2-1215148716)" /><text x="260.000000" y="437.000000" fill="#0000B8" class="text-mono-italic fill-N2" style="text-anchor:middle;font-size:16px">YES</text></g><g class="KHEyIC0mZ3Q7IGFnZylbMF0="><path d="M 431.999966 373.000000 L 431.998993 431.000000 S 431.998993 431.000000 431.998993 431.000000 L 580.166016 431.000000 S 580.166016 431.000000 580.166016 431.000000 L 580.166016 1046.000000" stroke="#000410" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-d2-1215148716-3488378134)" mask="url(#d2-1215148716)" /><text x="580.000000" y="642.000000" fill="#0000B8" class="text-mono-italic fill-N2" style="text-anchor:middle;font-size:16px">NO</text></g><g class="KHEzIC0mZ3Q7IGRpc2FnZylbMF0="><path d="M 152.998545 592.999999 L 152.668911 1046.000001" stroke="#000410" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-d2-1215148716-3488378134)" mask="url(#d2-1215148716)" /><text x="153.000000" y="826.000000" fill="#0000B8" class="text-mono-italic fill-N2" style="text-anchor:middle;font-size:16px">YES</text></g><g class="KHEzIC0mZ3Q7IHE0KVswXQ=="><path d="M 345.000000 598.000000 L 345.000000 740.000000" stroke="#000410" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-d2-1215148716-3488378134)" mask="url(#d2-1215148716)" /><text x="345.000000" y="676.000000" fill="#0000B8" class="text-mono-italic fill-N2" style="text-anchor:middle;font-size:16px">NO</text></g><g class="KHE0IC0mZ3Q7IGRpc2FnZylbMF0="><path d="M 262.998482 831.999999 L 262.836044 1046.000001" stroke="#000410" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-d2-1215148716-3488378134)" mask="url(#d2-1215148716)" /><text x="263.000000" y="946.000000" fill="#0000B8" class="text-mono-italic fill-N2" style="text-anchor:middle;font-size:16px">YES</text></g><g class="KHE0IC0mZ3Q7IGFnZylbMF0="><path d="M 397.002012 846.999999 L 397.165985 1010.000000 S 397.165985 1010.000000 397.165985 1010.000000 L 506.998993 1010.000000 S 506.998993 1010.000000 506.998993 1010.000000 L 506.998993 1046.000000" stroke="#000410" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-d2-1215148716-3488378134)" mask="url(#d2-1215148716)" /><text x="397.000000" y="1008.000000" fill="#0000B8" class="text-mono-italic fill-N2" style="text-anchor:middle;font-size:16px">NO</text></g><mask id="d2-1215148716" maskUnits="userSpaceOnUse" x="-9" y="-9" width="821" height="1160">
<rect x="-9" y="-9" width="821" height="1160" fill="white"></rect>
<rect x="367.000000" y="178.000000" width="32" height="21" fill="black"></rect>
<rect x="663.000000" y="555.000000" width="24" height="21" fill="black"></rect>
<rect x="244.000000" y="421.000000" width="32" height="21" fill="black"></rect>
<rect x="568.000000" y="626.000000" width="24" height="21" fill="black"></rect>
<rect x="137.000000" y="810.000000" width="32" height="21" fill="black"></rect>
<rect x="333.000000" y="660.000000" width="24" height="21" fill="black"></rect>
<rect x="247.000000" y="930.000000" width="32" height="21" fill="black"></rect>
<rect x="385.000000" y="992.000000" width="24" height="21" fill="black"></rect>
</mask></svg></svg>
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" data-d2-version="0.7.1" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1940 206"><svg class="d2-2903904730 d2-svg" width="1940" height="206" viewBox="-29 -29 1940 206"><rect x="-29.000000" y="-29.000000" width="1940.000000" height="206.000000" rx="0.000000" fill="transparent" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-2903904730 .text {
font-family: "d2-2903904730-font-regular";
}
@font-face {
font-family: d2-2903904730-font-regular;
src: url("data:application/font-woff;base64,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");
}
@font-face {
font-family: d2-2903904730-font-semibold;
src: url("data:application/font-woff;base64,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");
}
.d2-2903904730 .text-mono {
font-family: "d2-2903904730-font-mono";
}
@font-face {
font-family: d2-2903904730-font-mono;
src: url("data:application/font-woff;base64,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");
}
@media (prefers-color-scheme: dark) {
/* Text and connection colors: near-black -> near-white */
.d2-2903904730 .fill-N1 { fill: #E8E8E8; }
.d2-2903904730 .fill-N2 { fill: #CCCCCC; }
.d2-2903904730 .fill-N3 { fill: #999999; }
.d2-2903904730 .fill-B1 { fill: #E8E8E8; }
.d2-2903904730 .fill-B2 { fill: #7BACFF; }
.d2-2903904730 .fill-B3 { fill: #6AAFDC; }
/* Container and node fills: light -> dark */
.d2-2903904730 .fill-N4 { fill: #3A3A44; }
.d2-2903904730 .fill-N5 { fill: #2E2E38; }
.d2-2903904730 .fill-N6 { fill: #252530; }
.d2-2903904730 .fill-N7 { fill: transparent; }
.d2-2903904730 .fill-B4 { fill: #2A2A34; }
.d2-2903904730 .fill-B5 { fill: #1E1E28; }
.d2-2903904730 .fill-B6 { fill: #16161E; }
/* Strokes: near-black -> near-white */
.d2-2903904730 .stroke-N1 { stroke: #E8E8E8; }
.d2-2903904730 .stroke-N2 { stroke: #CCCCCC; }
.d2-2903904730 .stroke-N3 { stroke: #999999; }
.d2-2903904730 .stroke-N4 { stroke: #555555; }
.d2-2903904730 .stroke-N5 { stroke: #444444; }
.d2-2903904730 .stroke-N6 { stroke: #333333; }
.d2-2903904730 .stroke-N7 { stroke: transparent; }
.d2-2903904730 .stroke-B1 { stroke: #E8E8E8; }
.d2-2903904730 .stroke-B2 { stroke: #7BACFF; }
.d2-2903904730 .stroke-B3 { stroke: #6AAFDC; }
.d2-2903904730 .stroke-B4 { stroke: #3A3A44; }
.d2-2903904730 .stroke-B5 { stroke: #2E2E38; }
.d2-2903904730 .stroke-B6 { stroke: #252530; }
/* Text colors */
.d2-2903904730 .color-N1 { color: #E8E8E8; }
.d2-2903904730 .color-N2 { color: #CCCCCC; }
.d2-2903904730 .color-N3 { color: #999999; }
/* Connection marker fills (arrowheads) */
.d2-2903904730 .connection.fill-B1 { fill: #E8E8E8; }
}
]]></style><style type="text/css"><![CDATA[.shape {
shape-rendering: geometricPrecision;
stroke-linejoin: round;
}
.connection {
stroke-linecap: round;
stroke-linejoin: round;
}
.blend {
mix-blend-mode: multiply;
opacity: 0.5;
}
.d2-2903904730 .fill-N1{fill:#000410;}
.d2-2903904730 .fill-N2{fill:#0000B8;}
.d2-2903904730 .fill-N3{fill:#9499AB;}
.d2-2903904730 .fill-N4{fill:#CFD2DD;}
.d2-2903904730 .fill-N5{fill:#C3DEF3;}
.d2-2903904730 .fill-N6{fill:#EEF1F8;}
.d2-2903904730 .fill-N7{fill:#FFFFFF;}
.d2-2903904730 .fill-B1{fill:#000410;}
.d2-2903904730 .fill-B2{fill:#0000E4;}
.d2-2903904730 .fill-B3{fill:#5AA4DC;}
.d2-2903904730 .fill-B4{fill:#E7E9EE;}
.d2-2903904730 .fill-B5{fill:#F5F6F9;}
.d2-2903904730 .fill-B6{fill:#FFFFFF;}
.d2-2903904730 .fill-AA2{fill:#008566;}
.d2-2903904730 .fill-AA4{fill:#45BBA5;}
.d2-2903904730 .fill-AA5{fill:#7ACCBD;}
.d2-2903904730 .fill-AB4{fill:#F1C759;}
.d2-2903904730 .fill-AB5{fill:#F9E088;}
.d2-2903904730 .stroke-N1{stroke:#000410;}
.d2-2903904730 .stroke-N2{stroke:#0000B8;}
.d2-2903904730 .stroke-N3{stroke:#9499AB;}
.d2-2903904730 .stroke-N4{stroke:#CFD2DD;}
.d2-2903904730 .stroke-N5{stroke:#C3DEF3;}
.d2-2903904730 .stroke-N6{stroke:#EEF1F8;}
.d2-2903904730 .stroke-N7{stroke:#FFFFFF;}
.d2-2903904730 .stroke-B1{stroke:#000410;}
.d2-2903904730 .stroke-B2{stroke:#0000E4;}
.d2-2903904730 .stroke-B3{stroke:#5AA4DC;}
.d2-2903904730 .stroke-B4{stroke:#E7E9EE;}
.d2-2903904730 .stroke-B5{stroke:#F5F6F9;}
.d2-2903904730 .stroke-B6{stroke:#FFFFFF;}
.d2-2903904730 .stroke-AA2{stroke:#008566;}
.d2-2903904730 .stroke-AA4{stroke:#45BBA5;}
.d2-2903904730 .stroke-AA5{stroke:#7ACCBD;}
.d2-2903904730 .stroke-AB4{stroke:#F1C759;}
.d2-2903904730 .stroke-AB5{stroke:#F9E088;}
.d2-2903904730 .background-color-N1{background-color:#000410;}
.d2-2903904730 .background-color-N2{background-color:#0000B8;}
.d2-2903904730 .background-color-N3{background-color:#9499AB;}
.d2-2903904730 .background-color-N4{background-color:#CFD2DD;}
.d2-2903904730 .background-color-N5{background-color:#C3DEF3;}
.d2-2903904730 .background-color-N6{background-color:#EEF1F8;}
.d2-2903904730 .background-color-N7{background-color:#FFFFFF;}
.d2-2903904730 .background-color-B1{background-color:#000410;}
.d2-2903904730 .background-color-B2{background-color:#0000E4;}
.d2-2903904730 .background-color-B3{background-color:#5AA4DC;}
.d2-2903904730 .background-color-B4{background-color:#E7E9EE;}
.d2-2903904730 .background-color-B5{background-color:#F5F6F9;}
.d2-2903904730 .background-color-B6{background-color:#FFFFFF;}
.d2-2903904730 .background-color-AA2{background-color:#008566;}
.d2-2903904730 .background-color-AA4{background-color:#45BBA5;}
.d2-2903904730 .background-color-AA5{background-color:#7ACCBD;}
.d2-2903904730 .background-color-AB4{background-color:#F1C759;}
.d2-2903904730 .background-color-AB5{background-color:#F9E088;}
.d2-2903904730 .color-N1{color:#000410;}
.d2-2903904730 .color-N2{color:#0000B8;}
.d2-2903904730 .color-N3{color:#9499AB;}
.d2-2903904730 .color-N4{color:#CFD2DD;}
.d2-2903904730 .color-N5{color:#C3DEF3;}
.d2-2903904730 .color-N6{color:#EEF1F8;}
.d2-2903904730 .color-N7{color:#FFFFFF;}
.d2-2903904730 .color-B1{color:#000410;}
.d2-2903904730 .color-B2{color:#0000E4;}
.d2-2903904730 .color-B3{color:#5AA4DC;}
.d2-2903904730 .color-B4{color:#E7E9EE;}
.d2-2903904730 .color-B5{color:#F5F6F9;}
.d2-2903904730 .color-B6{color:#FFFFFF;}
.d2-2903904730 .color-AA2{color:#008566;}
.d2-2903904730 .color-AA4{color:#45BBA5;}
.d2-2903904730 .color-AA5{color:#7ACCBD;}
.d2-2903904730 .color-AB4{color:#F1C759;}
.d2-2903904730 .color-AB5{color:#F9E088;}.appendix text.text{fill:#000410}.md{--color-fg-default:#000410;--color-fg-muted:#0000B8;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#000410;--color-border-muted:#0000E4;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0000E4;--color-accent-emphasis:#0000E4;--color-attention-subtle:#0000B8;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker-d2-2903904730);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker-d2-2903904730);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-normal-d2-2903904730);mix-blend-mode:color-burn}.sketch-overlay-B4{fill:url(#streaks-bright-d2-2903904730);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright-d2-2903904730);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright-d2-2903904730);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark-d2-2903904730);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-normal-d2-2903904730);mix-blend-mode:color-burn}.sketch-overlay-AA5{fill:url(#streaks-normal-d2-2903904730);mix-blend-mode:color-burn}.sketch-overlay-AB4{fill:url(#streaks-normal-d2-2903904730);mix-blend-mode:color-burn}.sketch-overlay-AB5{fill:url(#streaks-normal-d2-2903904730);mix-blend-mode:color-burn}.sketch-overlay-N1{fill:url(#streaks-darker-d2-2903904730);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-darker-d2-2903904730);mix-blend-mode:lighten}.sketch-overlay-N3{fill:url(#streaks-normal-d2-2903904730);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal-d2-2903904730);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-normal-d2-2903904730);mix-blend-mode:color-burn}.sketch-overlay-N6{fill:url(#streaks-bright-d2-2903904730);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright-d2-2903904730);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}
@media (prefers-color-scheme: dark) {
/* Text and connection colors: near-black -> near-white */
.d2-2903904730 .fill-N1 { fill: #E8E8E8; }
.d2-2903904730 .fill-N2 { fill: #CCCCCC; }
.d2-2903904730 .fill-N3 { fill: #999999; }
.d2-2903904730 .fill-B1 { fill: #E8E8E8; }
.d2-2903904730 .fill-B2 { fill: #7BACFF; }
.d2-2903904730 .fill-B3 { fill: #6AAFDC; }
/* Container and node fills: light -> dark */
.d2-2903904730 .fill-N4 { fill: #3A3A44; }
.d2-2903904730 .fill-N5 { fill: #2E2E38; }
.d2-2903904730 .fill-N6 { fill: #252530; }
.d2-2903904730 .fill-N7 { fill: transparent; }
.d2-2903904730 .fill-B4 { fill: #2A2A34; }
.d2-2903904730 .fill-B5 { fill: #1E1E28; }
.d2-2903904730 .fill-B6 { fill: #16161E; }
/* Strokes: near-black -> near-white */
.d2-2903904730 .stroke-N1 { stroke: #E8E8E8; }
.d2-2903904730 .stroke-N2 { stroke: #CCCCCC; }
.d2-2903904730 .stroke-N3 { stroke: #999999; }
.d2-2903904730 .stroke-N4 { stroke: #555555; }
.d2-2903904730 .stroke-N5 { stroke: #444444; }
.d2-2903904730 .stroke-N6 { stroke: #333333; }
.d2-2903904730 .stroke-N7 { stroke: transparent; }
.d2-2903904730 .stroke-B1 { stroke: #E8E8E8; }
.d2-2903904730 .stroke-B2 { stroke: #7BACFF; }
.d2-2903904730 .stroke-B3 { stroke: #6AAFDC; }
.d2-2903904730 .stroke-B4 { stroke: #3A3A44; }
.d2-2903904730 .stroke-B5 { stroke: #2E2E38; }
.d2-2903904730 .stroke-B6 { stroke: #252530; }
/* Text colors */
.d2-2903904730 .color-N1 { color: #E8E8E8; }
.d2-2903904730 .color-N2 { color: #CCCCCC; }
.d2-2903904730 .color-N3 { color: #999999; }
/* Connection marker fills (arrowheads) */
.d2-2903904730 .connection.fill-B1 { fill: #E8E8E8; }
}
]]></style><style type="text/css">.d2-2903904730 .md em,
.d2-2903904730 .md dfn {
font-family: "d2-2903904730-font-italic";
}
.d2-2903904730 .md b,
.d2-2903904730 .md strong {
font-family: "d2-2903904730-font-bold";
}
.d2-2903904730 .md code,
.d2-2903904730 .md kbd,
.d2-2903904730 .md pre,
.d2-2903904730 .md samp {
font-family: "d2-2903904730-font-mono";
font-size: 1em;
}
.d2-2903904730 .md {
tab-size: 4;
}
/* variables are provided in d2renderers/d2svg/d2svg.go */
.d2-2903904730 .md {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
margin: 0;
background-color: transparent; /* we don't want to define the background color */
font-family: "d2-2903904730-font-regular";
font-size: 16px;
line-height: 1.5;
word-wrap: break-word;
}
.d2-2903904730 .md details,
.d2-2903904730 .md figcaption,
.d2-2903904730 .md figure {
display: block;
}
.d2-2903904730 .md summary {
display: list-item;
}
.d2-2903904730 .md [hidden] {
display: none !important;
}
.d2-2903904730 .md a {
background-color: transparent;
color: var(--color-accent-fg);
text-decoration: none;
}
.d2-2903904730 .md a:active,
.d2-2903904730 .md a:hover {
outline-width: 0;
}
.d2-2903904730 .md abbr[title] {
border-bottom: none;
text-decoration: underline dotted;
}
.d2-2903904730 .md dfn {
font-style: italic;
}
.d2-2903904730 .md h1 {
margin: 0.67em 0;
padding-bottom: 0.3em;
font-size: 2em;
border-bottom: 1px solid var(--color-border-muted);
}
.d2-2903904730 .md mark {
background-color: var(--color-attention-subtle);
color: var(--color-text-primary);
}
.d2-2903904730 .md small {
font-size: 90%;
}
.d2-2903904730 .md sub,
.d2-2903904730 .md sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
.d2-2903904730 .md sub {
bottom: -0.25em;
}
.d2-2903904730 .md sup {
top: -0.5em;
}
.d2-2903904730 .md img {
border-style: none;
max-width: 100%;
box-sizing: content-box;
background-color: var(--color-canvas-default);
}
.d2-2903904730 .md figure {
margin: 1em 40px;
}
.d2-2903904730 .md hr {
box-sizing: content-box;
overflow: hidden;
background: transparent;
border-bottom: 1px solid var(--color-border-muted);
height: 0.25em;
padding: 0;
margin: 24px 0;
background-color: var(--color-border-default);
border: 0;
}
.d2-2903904730 .md input {
font: inherit;
margin: 0;
overflow: visible;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
.d2-2903904730 .md [type="button"],
.d2-2903904730 .md [type="reset"],
.d2-2903904730 .md [type="submit"] {
-webkit-appearance: button;
}
.d2-2903904730 .md [type="button"]::-moz-focus-inner,
.d2-2903904730 .md [type="reset"]::-moz-focus-inner,
.d2-2903904730 .md [type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
.d2-2903904730 .md [type="button"]:-moz-focusring,
.d2-2903904730 .md [type="reset"]:-moz-focusring,
.d2-2903904730 .md [type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
.d2-2903904730 .md [type="checkbox"],
.d2-2903904730 .md [type="radio"] {
box-sizing: border-box;
padding: 0;
}
.d2-2903904730 .md [type="number"]::-webkit-inner-spin-button,
.d2-2903904730 .md [type="number"]::-webkit-outer-spin-button {
height: auto;
}
.d2-2903904730 .md [type="search"] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
.d2-2903904730 .md [type="search"]::-webkit-search-cancel-button,
.d2-2903904730 .md [type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
.d2-2903904730 .md ::-webkit-input-placeholder {
color: inherit;
opacity: 0.54;
}
.d2-2903904730 .md ::-webkit-file-upload-button {
-webkit-appearance: button;
font: inherit;
}
.d2-2903904730 .md a:hover {
text-decoration: underline;
}
.d2-2903904730 .md hr::before {
display: table;
content: "";
}
.d2-2903904730 .md hr::after {
display: table;
clear: both;
content: "";
}
.d2-2903904730 .md table {
border-spacing: 0;
border-collapse: collapse;
display: block;
width: max-content;
max-width: 100%;
overflow: auto;
}
.d2-2903904730 .md td,
.d2-2903904730 .md th {
padding: 0;
}
.d2-2903904730 .md details summary {
cursor: pointer;
}
.d2-2903904730 .md details:not([open]) > *:not(summary) {
display: none !important;
}
.d2-2903904730 .md kbd {
display: inline-block;
padding: 3px 5px;
color: var(--color-fg-default);
vertical-align: middle;
background-color: var(--color-canvas-subtle);
border: solid 1px var(--color-neutral-muted);
border-bottom-color: var(--color-neutral-muted);
border-radius: 6px;
box-shadow: inset 0 -1px 0 var(--color-neutral-muted);
}
.d2-2903904730 .md h1,
.d2-2903904730 .md h2,
.d2-2903904730 .md h3,
.d2-2903904730 .md h4,
.d2-2903904730 .md h5,
.d2-2903904730 .md h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 400;
line-height: 1.25;
font-family: "d2-2903904730-font-semibold";
}
.d2-2903904730 .md h2 {
padding-bottom: 0.3em;
font-size: 1.5em;
border-bottom: 1px solid var(--color-border-muted);
}
.d2-2903904730 .md h3 {
font-size: 1.25em;
}
.d2-2903904730 .md h4 {
font-size: 1em;
}
.d2-2903904730 .md h5 {
font-size: 0.875em;
}
.d2-2903904730 .md h6 {
font-size: 0.85em;
color: var(--color-fg-muted);
}
.d2-2903904730 .md p {
margin-top: 0;
margin-bottom: 10px;
}
.d2-2903904730 .md blockquote {
margin: 0;
padding: 0 1em;
color: var(--color-fg-muted);
border-left: 0.25em solid var(--color-border-default);
}
.d2-2903904730 .md ul,
.d2-2903904730 .md ol {
margin-top: 0;
margin-bottom: 0;
padding-left: 2em;
}
.d2-2903904730 .md ol ol,
.d2-2903904730 .md ul ol {
list-style-type: lower-roman;
}
.d2-2903904730 .md ul ul ol,
.d2-2903904730 .md ul ol ol,
.d2-2903904730 .md ol ul ol,
.d2-2903904730 .md ol ol ol {
list-style-type: lower-alpha;
}
.d2-2903904730 .md dd {
margin-left: 0;
}
.d2-2903904730 .md pre {
margin-top: 0;
margin-bottom: 0;
word-wrap: normal;
}
.d2-2903904730 .md ::placeholder {
color: var(--color-fg-subtle);
opacity: 1;
}
.d2-2903904730 .md input::-webkit-outer-spin-button,
.d2-2903904730 .md input::-webkit-inner-spin-button {
margin: 0;
-webkit-appearance: none;
appearance: none;
}
.d2-2903904730 .md::before {
display: table;
content: "";
}
.d2-2903904730 .md::after {
display: table;
clear: both;
content: "";
}
.d2-2903904730 .md > *:first-child {
margin-top: 0 !important;
}
.d2-2903904730 .md > *:last-child {
margin-bottom: 0 !important;
}
.d2-2903904730 .md a:not([href]) {
color: inherit;
text-decoration: none;
}
.d2-2903904730 .md .absent {
color: var(--color-danger-fg);
}
.d2-2903904730 .md .anchor {
float: left;
padding-right: 4px;
margin-left: -20px;
line-height: 1;
}
.d2-2903904730 .md .anchor:focus {
outline: none;
}
.d2-2903904730 .md p,
.d2-2903904730 .md blockquote,
.d2-2903904730 .md ul,
.d2-2903904730 .md ol,
.d2-2903904730 .md dl,
.d2-2903904730 .md table,
.d2-2903904730 .md pre,
.d2-2903904730 .md details {
margin-top: 0;
margin-bottom: 16px;
}
.d2-2903904730 .md blockquote > :first-child {
margin-top: 0;
}
.d2-2903904730 .md blockquote > :last-child {
margin-bottom: 0;
}
.d2-2903904730 .md sup > a::before {
content: "[";
}
.d2-2903904730 .md sup > a::after {
content: "]";
}
.d2-2903904730 .md h1:hover .anchor,
.d2-2903904730 .md h2:hover .anchor,
.d2-2903904730 .md h3:hover .anchor,
.d2-2903904730 .md h4:hover .anchor,
.d2-2903904730 .md h5:hover .anchor,
.d2-2903904730 .md h6:hover .anchor {
text-decoration: none;
}
.d2-2903904730 .md h1 tt,
.d2-2903904730 .md h1 code,
.d2-2903904730 .md h2 tt,
.d2-2903904730 .md h2 code,
.d2-2903904730 .md h3 tt,
.d2-2903904730 .md h3 code,
.d2-2903904730 .md h4 tt,
.d2-2903904730 .md h4 code,
.d2-2903904730 .md h5 tt,
.d2-2903904730 .md h5 code,
.d2-2903904730 .md h6 tt,
.d2-2903904730 .md h6 code {
padding: 0 0.2em;
font-size: inherit;
}
.d2-2903904730 .md ul.no-list,
.d2-2903904730 .md ol.no-list {
padding: 0;
list-style-type: none;
}
.d2-2903904730 .md ol[type="1"] {
list-style-type: decimal;
}
.d2-2903904730 .md ol[type="a"] {
list-style-type: lower-alpha;
}
.d2-2903904730 .md ol[type="i"] {
list-style-type: lower-roman;
}
.d2-2903904730 .md div > ol:not([type]) {
list-style-type: decimal;
}
.d2-2903904730 .md ul ul,
.d2-2903904730 .md ul ol,
.d2-2903904730 .md ol ol,
.d2-2903904730 .md ol ul {
margin-top: 0;
margin-bottom: 0;
}
.d2-2903904730 .md li > p {
margin-top: 16px;
}
.d2-2903904730 .md li + li {
margin-top: 0.25em;
}
.d2-2903904730 .md dl {
padding: 0;
}
.d2-2903904730 .md dl dt {
padding: 0;
margin-top: 16px;
font-size: 1em;
font-style: italic;
font-family: "d2-2903904730-font-semibold";
}
.d2-2903904730 .md dl dd {
padding: 0 16px;
margin-bottom: 16px;
}
.d2-2903904730 .md table th {
font-family: "d2-2903904730-font-semibold";
}
.d2-2903904730 .md table th,
.d2-2903904730 .md table td {
padding: 6px 13px;
border: 1px solid var(--color-border-default);
}
.d2-2903904730 .md table tr {
background-color: var(--color-canvas-default);
border-top: 1px solid var(--color-border-muted);
}
.d2-2903904730 .md table tr:nth-child(2n) {
background-color: var(--color-canvas-subtle);
}
.d2-2903904730 .md table img {
background-color: transparent;
}
.d2-2903904730 .md img[align="right"] {
padding-left: 20px;
}
.d2-2903904730 .md img[align="left"] {
padding-right: 20px;
}
.d2-2903904730 .md span.frame {
display: block;
overflow: hidden;
}
.d2-2903904730 .md span.frame > span {
display: block;
float: left;
width: auto;
padding: 7px;
margin: 13px 0 0;
overflow: hidden;
border: 1px solid var(--color-border-default);
}
.d2-2903904730 .md span.frame span img {
display: block;
float: left;
}
.d2-2903904730 .md span.frame span span {
display: block;
padding: 5px 0 0;
clear: both;
color: var(--color-fg-default);
}
.d2-2903904730 .md span.align-center {
display: block;
overflow: hidden;
clear: both;
}
.d2-2903904730 .md span.align-center > span {
display: block;
margin: 13px auto 0;
overflow: hidden;
text-align: center;
}
.d2-2903904730 .md span.align-center span img {
margin: 0 auto;
text-align: center;
}
.d2-2903904730 .md span.align-right {
display: block;
overflow: hidden;
clear: both;
}
.d2-2903904730 .md span.align-right > span {
display: block;
margin: 13px 0 0;
overflow: hidden;
text-align: right;
}
.d2-2903904730 .md span.align-right span img {
margin: 0;
text-align: right;
}
.d2-2903904730 .md span.float-left {
display: block;
float: left;
margin-right: 13px;
overflow: hidden;
}
.d2-2903904730 .md span.float-left span {
margin: 13px 0 0;
}
.d2-2903904730 .md span.float-right {
display: block;
float: right;
margin-left: 13px;
overflow: hidden;
}
.d2-2903904730 .md span.float-right > span {
display: block;
margin: 13px auto 0;
overflow: hidden;
text-align: right;
}
.d2-2903904730 .md code,
.d2-2903904730 .md tt {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
background-color: var(--color-neutral-muted);
border-radius: 6px;
}
.d2-2903904730 .md code br,
.d2-2903904730 .md tt br {
display: none;
}
.d2-2903904730 .md del code {
text-decoration: inherit;
}
.d2-2903904730 .md pre code {
font-size: 100%;
}
.d2-2903904730 .md pre > code {
padding: 0;
margin: 0;
word-break: normal;
white-space: pre;
background: transparent;
border: 0;
}
.d2-2903904730 .md .highlight {
margin-bottom: 16px;
}
.d2-2903904730 .md .highlight pre {
margin-bottom: 0;
word-break: normal;
}
.d2-2903904730 .md .highlight pre,
.d2-2903904730 .md pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: var(--color-canvas-subtle);
border-radius: 6px;
}
.d2-2903904730 .md pre code,
.d2-2903904730 .md pre tt {
display: inline;
max-width: auto;
padding: 0;
margin: 0;
overflow: visible;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
border: 0;
}
.d2-2903904730 .md .csv-data td,
.d2-2903904730 .md .csv-data th {
padding: 5px;
overflow: hidden;
font-size: 12px;
line-height: 1;
text-align: left;
white-space: nowrap;
}
.d2-2903904730 .md .csv-data .blob-num {
padding: 10px 8px 9px;
text-align: right;
background: var(--color-canvas-default);
border: 0;
}
.d2-2903904730 .md .csv-data tr {
border-top: 0;
}
.d2-2903904730 .md .csv-data th {
font-family: "d2-2903904730-font-semibold";
background: var(--color-canvas-subtle);
border-top: 0;
}
.d2-2903904730 .md .footnotes {
font-size: 12px;
color: var(--color-fg-muted);
border-top: 1px solid var(--color-border-default);
}
.d2-2903904730 .md .footnotes ol {
padding-left: 16px;
}
.d2-2903904730 .md .footnotes li {
position: relative;
}
.d2-2903904730 .md .footnotes li:target::before {
position: absolute;
top: -8px;
right: -8px;
bottom: -8px;
left: -24px;
pointer-events: none;
content: "";
border: 2px solid var(--color-accent-emphasis);
border-radius: 6px;
}
.d2-2903904730 .md .footnotes li:target {
color: var(--color-fg-default);
}
.d2-2903904730 .md .task-list-item {
list-style-type: none;
}
.d2-2903904730 .md .task-list-item label {
font-weight: 400;
}
.d2-2903904730 .md .task-list-item.enabled label {
cursor: pointer;
}
.d2-2903904730 .md .task-list-item + .task-list-item {
margin-top: 3px;
}
.d2-2903904730 .md .task-list-item .handle {
display: none;
}
.d2-2903904730 .md .task-list-item-checkbox {
margin: 0 0.2em 0.25em -1.6em;
vertical-align: middle;
}
.d2-2903904730 .md .contains-task-list:dir(rtl) .task-list-item-checkbox {
margin: 0 -1.6em 0.25em 0.2em;
}
</style><style type="text/css"><![CDATA[
.dots-overlay {
fill: url(#dots-d2-2903904730);
mix-blend-mode: multiply;
}
@media (prefers-color-scheme: dark) {
/* Text and connection colors: near-black -> near-white */
.d2-2903904730 .fill-N1 { fill: #E8E8E8; }
.d2-2903904730 .fill-N2 { fill: #CCCCCC; }
.d2-2903904730 .fill-N3 { fill: #999999; }
.d2-2903904730 .fill-B1 { fill: #E8E8E8; }
.d2-2903904730 .fill-B2 { fill: #7BACFF; }
.d2-2903904730 .fill-B3 { fill: #6AAFDC; }
/* Container and node fills: light -> dark */
.d2-2903904730 .fill-N4 { fill: #3A3A44; }
.d2-2903904730 .fill-N5 { fill: #2E2E38; }
.d2-2903904730 .fill-N6 { fill: #252530; }
.d2-2903904730 .fill-N7 { fill: transparent; }
.d2-2903904730 .fill-B4 { fill: #2A2A34; }
.d2-2903904730 .fill-B5 { fill: #1E1E28; }
.d2-2903904730 .fill-B6 { fill: #16161E; }
/* Strokes: near-black -> near-white */
.d2-2903904730 .stroke-N1 { stroke: #E8E8E8; }
.d2-2903904730 .stroke-N2 { stroke: #CCCCCC; }
.d2-2903904730 .stroke-N3 { stroke: #999999; }
.d2-2903904730 .stroke-N4 { stroke: #555555; }
.d2-2903904730 .stroke-N5 { stroke: #444444; }
.d2-2903904730 .stroke-N6 { stroke: #333333; }
.d2-2903904730 .stroke-N7 { stroke: transparent; }
.d2-2903904730 .stroke-B1 { stroke: #E8E8E8; }
.d2-2903904730 .stroke-B2 { stroke: #7BACFF; }
.d2-2903904730 .stroke-B3 { stroke: #6AAFDC; }
.d2-2903904730 .stroke-B4 { stroke: #3A3A44; }
.d2-2903904730 .stroke-B5 { stroke: #2E2E38; }
.d2-2903904730 .stroke-B6 { stroke: #252530; }
/* Text colors */
.d2-2903904730 .color-N1 { color: #E8E8E8; }
.d2-2903904730 .color-N2 { color: #CCCCCC; }
.d2-2903904730 .color-N3 { color: #999999; }
/* Connection marker fills (arrowheads) */
.d2-2903904730 .connection.fill-B1 { fill: #E8E8E8; }
}
]]></style><defs><pattern id="dots-d2-2903904730" x="0" y="0" width="15" height="15" patternUnits="userSpaceOnUse">
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="2" y="2" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="12" y="2" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="12" y="12" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="2" y="12" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="2" y="7" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="12" y="7" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="7" y="2" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="7" y="12" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="7" y="7" width="1" height="1" fill="#0A0F25"/>
</g>
</pattern>
</defs><g class="aW5zdGFsbA=="><g class="shape" ><rect x="12.000000" y="12.000000" width="360.000000" height="124.000000" stroke="#000410" fill="#E7E9EE" class=" stroke-B1 fill-B4" style="stroke-width:2;" /><rect x="12.000000" y="12.000000" width="360.000000" height="124.000000" class="dots-overlay" style="stroke-width:2;" /><rect x="17.000000" y="17.000000" width="350.000000" height="114.000000" stroke="#000410" fill="transparent" class=" stroke-B1" style="stroke-width:2;" /></g><text x="192.000000" y="45.000000" fill="#000410" class="text-mono fill-N1" style="text-anchor:middle;font-size:28px">INSTALL</text></g><g class="Y29uZmlndXJl"><g class="shape" ><rect x="442.000000" y="12.000000" width="331.000000" height="124.000000" stroke="#000410" fill="#E7E9EE" class=" stroke-B1 fill-B4" style="stroke-width:2;" /><rect x="442.000000" y="12.000000" width="331.000000" height="124.000000" class="dots-overlay" style="stroke-width:2;" /><rect x="447.000000" y="17.000000" width="321.000000" height="114.000000" stroke="#000410" fill="transparent" class=" stroke-B1" style="stroke-width:2;" /></g><text x="607.500000" y="45.000000" fill="#000410" class="text-mono fill-N1" style="text-anchor:middle;font-size:28px">CONFIGURE</text></g><g class="Y29tcGFyZQ=="><g class="shape" ><rect x="843.000000" y="12.000000" width="312.000000" height="124.000000" stroke="#000410" fill="#E7E9EE" class=" stroke-B1 fill-B4" style="stroke-width:2;" /><rect x="843.000000" y="12.000000" width="312.000000" height="124.000000" class="dots-overlay" style="stroke-width:2;" /><rect x="848.000000" y="17.000000" width="302.000000" height="114.000000" stroke="#000410" fill="transparent" class=" stroke-B1" style="stroke-width:2;" /></g><text x="999.000000" y="45.000000" fill="#000410" class="text-mono fill-N1" style="text-anchor:middle;font-size:28px">COMPARE</text></g><g class="ZGVwbG95"><g class="shape" ><rect x="1225.000000" y="12.000000" width="273.000000" height="124.000000" stroke="#000410" fill="#E7E9EE" class=" stroke-B1 fill-B4" style="stroke-width:2;" /><rect x="1225.000000" y="12.000000" width="273.000000" height="124.000000" class="dots-overlay" style="stroke-width:2;" /><rect x="1230.000000" y="17.000000" width="263.000000" height="114.000000" stroke="#000410" fill="transparent" class=" stroke-B1" style="stroke-width:2;" /></g><text x="1361.500000" y="45.000000" fill="#000410" class="text-mono fill-N1" style="text-anchor:middle;font-size:28px">DEPLOY</text></g><g class="dmFsaWRhdGU="><g class="shape" ><rect x="1568.000000" y="12.000000" width="302.000000" height="124.000000" stroke="#000410" fill="#E7E9EE" class=" stroke-B1 fill-B4" style="stroke-width:2;" /><rect x="1568.000000" y="12.000000" width="302.000000" height="124.000000" class="dots-overlay" style="stroke-width:2;" /><rect x="1573.000000" y="17.000000" width="292.000000" height="114.000000" stroke="#000410" fill="transparent" class=" stroke-B1" style="stroke-width:2;" /></g><text x="1719.000000" y="45.000000" fill="#000410" class="text-mono fill-N1" style="text-anchor:middle;font-size:28px">VALIDATE</text></g><g class="aW5zdGFsbC5pbnN0YWxsX2RldGFpbA=="><g class="shape" ></g><g><foreignObject requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" x="62.000000" y="62.000000" width="260" height="24"><div xmlns="http://www.w3.org/1999/xhtml" class="md color-N1"><p>PIP3 INSTALL
AICONFIGURATOR</p>
</div></foreignObject></g></g><g class="Y29uZmlndXJlLmNvbmZpZ3VyZV9kZXRhaWw="><g class="shape" ></g><g><foreignObject requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" x="492.000000" y="62.000000" width="231" height="24"><div xmlns="http://www.w3.org/1999/xhtml" class="md color-N1"><p>MODEL, GPUS,
SLA TARGETS</p>
</div></foreignObject></g></g><g class="Y29tcGFyZS5jb21wYXJlX2RldGFpbA=="><g class="shape" ></g><g><foreignObject requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" x="893.000000" y="62.000000" width="212" height="24"><div xmlns="http://www.w3.org/1999/xhtml" class="md color-N1"><p>AGG VS DISAGG
RANKINGS</p>
</div></foreignObject></g></g><g class="ZGVwbG95LmRlcGxveV9kZXRhaWw="><g class="shape" ></g><g><foreignObject requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" x="1275.000000" y="62.000000" width="173" height="24"><div xmlns="http://www.w3.org/1999/xhtml" class="md color-N1"><p>APPLY DGD
MANIFEST</p>
</div></foreignObject></g></g><g class="dmFsaWRhdGUudmFsaWRhdGVfZGV0YWls"><g class="shape" ></g><g><foreignObject requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" x="1618.000000" y="62.000000" width="202" height="24"><div xmlns="http://www.w3.org/1999/xhtml" class="md color-N1"><p>BENCHMARK
WITH AIPERF</p>
</div></foreignObject></g></g><g class="KGluc3RhbGwgLSZndDsgY29uZmlndXJlKVswXQ=="><marker id="mk-d2-2903904730-3488378134" markerWidth="10.000000" markerHeight="12.000000" refX="7.000000" refY="6.000000" viewBox="0.000000 0.000000 10.000000 12.000000" orient="auto" markerUnits="userSpaceOnUse"> <polygon points="0.000000,0.000000 10.000000,6.000000 0.000000,12.000000" fill="#000410" class="connection fill-B1" stroke-width="2" /> </marker><path d="M 374.000000 74.000000 L 438.000000 74.000000" stroke="#000410" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-d2-2903904730-3488378134)" mask="url(#d2-2903904730)" /></g><g class="KGNvbmZpZ3VyZSAtJmd0OyBjb21wYXJlKVswXQ=="><path d="M 775.000000 74.000000 L 839.000000 74.000000" stroke="#000410" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-d2-2903904730-3488378134)" mask="url(#d2-2903904730)" /></g><g class="KGNvbXBhcmUgLSZndDsgZGVwbG95KVswXQ=="><path d="M 1157.000000 74.000000 L 1221.000000 74.000000" stroke="#000410" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-d2-2903904730-3488378134)" mask="url(#d2-2903904730)" /></g><g class="KGRlcGxveSAtJmd0OyB2YWxpZGF0ZSlbMF0="><path d="M 1500.000000 74.000000 L 1564.000000 74.000000" stroke="#000410" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-d2-2903904730-3488378134)" mask="url(#d2-2903904730)" /></g><mask id="d2-2903904730" maskUnits="userSpaceOnUse" x="-29" y="-29" width="1940" height="206">
<rect x="-29" y="-29" width="1940" height="206" fill="white"></rect>
</mask></svg></svg>
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment