LLM Inference on Trainium2 with vLLM & PyTorch Native

Qwen3-8B | bfloat16 | TLV Summit 2026
πŸ›οΈ Architecture
⚑ Live Inference
πŸš€ Performance Testing
πŸ“„ Serving Code
πŸ”§ PyTorch Native
πŸ“Š Neuron Explorer
☸️ vLLM on EKS

Reference Architecture β€” vLLM on Amazon EKS, GPU & Trainium side-by-side

One EKS cluster serves Qwen3-8B on both accelerators. Each pod owns its accelerator and runs vLLM; the Neuron device plugin exposes NeuronCores, and node scaling is handled by Karpenter / EKS Auto Mode (provisions the right instance per pod), not pod-level HPA.

☸️ Amazon EKS Cluster

GPU + Trainium node pools, one control plane
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ Amazon EKS control plane (managed) ──────────────────────────────┐
β”‚                                                                                                  β”‚
β”‚   Ingress / ALB  ──▢  vLLM Service  ──▢  routes by model / node pool                              β”‚
β”‚                                                                                                  β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ GPU node pool ───────────────────┐   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ Trainium node pool ────────────┐ β”‚
β”‚   β”‚  g6e.4xlarge  (1Γ— NVIDIA L40S, 48GB)             β”‚   β”‚  trn2.3xlarge (1Γ— Trainium2 chip, 4 cores) β”‚ β”‚
β”‚   β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”            β”‚   β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
β”‚   β”‚  β”‚ vLLM pod                           β”‚            β”‚   β”‚  β”‚ vLLM pod                                  β”‚ β”‚ β”‚
β”‚   β”‚  β”‚  Qwen3-8B Β· CUDA Β· TP=1            β”‚            β”‚   β”‚  β”‚  Qwen3-8B Β· Neuron Β· TP=4  β†’  4 cores    β”‚ β”‚ β”‚
β”‚   β”‚  β”‚  requests: nvidia.com/gpu: 1       β”‚            β”‚   β”‚  β”‚  requests: aws.amazon.com/neuron: 1       β”‚ β”‚ β”‚
β”‚   β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β”‚   β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β”‚   β”‚   NVIDIA device plugin                            β”‚   β”‚   Neuron device plugin (DaemonSet)         β”‚ β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚                                                                                                  β”‚
β”‚   Karpenter / EKS Auto Mode β€” provisions g6e or trn2 nodes on demand to match pending pods         β”‚
β”‚   Observability: neuron-monitor ─▢ CloudWatch   Β·   Neuron Explorer (offline profile viewer)      β”‚
β”‚                                                                                                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    

GPU node pool

β€’ g6e.4xlarge β€” 1Γ— NVIDIA L40S (48 GB)
β€’ vLLM, tensor-parallel = 1 (single GPU)
β€’ Pod requests nvidia.com/gpu: 1
β€’ NVIDIA device plugin advertises the GPU
β€’ $3.004/hr on-demand (us-east-2)

Trainium node pool

β€’ trn2.3xlarge β€” 1Γ— Trainium2 chip = 4 NeuronCores (96 GB)
β€’ vLLM, tensor-parallel = 4 β†’ one core per TP rank
β€’ Pod requests aws.amazon.com/neuron: 1 (whole device)
β€’ Neuron device plugin advertises cores/devices
β€’ $2.235/hr Capacity Block (sa-east-1)
Scaling β€” Karpenter / EKS Auto Mode (not HPA): pods declare their accelerator request; Karpenter watches pending pods and launches the right node type (g6e or trn2) to fit them, then consolidates when idle. Scale-out adds nodes/accelerators sized to the workload β€” a better fit for one-accelerator-per-pod LLM serving than replica-count HPA on a fixed node set.

🟒 NVIDIA L40S (GPU)

g6e.4xlarge | $3.004/hr
Ready
--
TTFT (ms)
--
tok/s
--
in tok
--
out tok
--
cost

🟣 AWS Trainium2

trn2.3xlarge (1 chip, 4 NeuronCores) | TP=4 | ~$2.24/hr
Ready
--
TTFT (ms)
--
tok/s
--
in tok
--
out tok
--
cost

🟒 NVIDIA L40S

g6e.4xlarge | $3.00/hr
Ready
--
tok/s (aggregate)
--
$/1M tokens
GPU Utilization (approx from throughput)
Tokens: --
Time: --s
Reqs: --

🟣 AWS Trainium2

trn2.3xlarge (1 chip, 4 NeuronCores) | TP=4 | ~$2.24/hr
Ready
--
tok/s (aggregate)
--
$/1M tokens
NeuronCore Utilization (live neuron-monitor)
Tokens: --
Time: --s
Reqs: --

GPU Serving

Standard CUDA path
# Instance: g6e.4xlarge (NVIDIA L40S, 46GB)
# Ubuntu 24.04 | CUDA 13.2 | vLLM 0.24

# Install
pip install vllm

# Serve
vllm serve Qwen/Qwen3-8B \
    --dtype bfloat16 \
    --max-model-len 2048 \
    --host 0.0.0.0 --port 8000

# Query (OpenAI-compatible API)
curl http://localhost:8000/v1/chat/completions \
  -d '{
    "model": "Qwen/Qwen3-8B",
    "messages": [{"role":"user","content":"Hello"}],
    "max_tokens": 256
  }'

Trainium Serving

Same API, different backend
# Instance: trn2.3xlarge (4 NeuronCores, 96GB)
# Ubuntu 24.04 | Neuron SDK 2.25 | vLLM 0.16 + NxDI

# Activate (pre-installed on Neuron DLAMI)
source /opt/aws_neuronx_venv_pytorch_inference_vllm_0_16/bin/activate

# Serve
export VLLM_NEURON_FRAMEWORK=neuronx-distributed-inference

vllm serve Qwen/Qwen3-8B \
    --tensor-parallel-size 4 \
    --max-model-len 2048 \
    --host 0.0.0.0 --port 8000

# Query β€” IDENTICAL API! Same curl command works.
curl http://localhost:8000/v1/chat/completions \
  -d '{
    "model": "Qwen/Qwen3-8B",
    "messages": [{"role":"user","content":"Hello"}],
    "max_tokens": 256
  }'

PyTorch Native on Neuron β€” Zero Friction Porting

With PyTorch Native, moving from GPU to Trainium is as simple as changing device='cuda' β†’ device='neuron' and using torch.compile(backend="neuron"). No custom compilation steps, no model rewrites β€” same PyTorch code, different hardware.

GPU Code (PyTorch + CUDA)

Standard PyTorch workflow
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

# Load model
model = AutoModelForCausalLM.from_pretrained(
    "Qwen/Qwen3-8B",
    torch_dtype=torch.bfloat16,
)

# Move to device
model = model.to("cuda")
model.eval()

# Compile for performance
model.forward = torch.compile(
    model.forward,
    # uses default inductor backend
)

# Tokenize
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-8B")
inputs = tokenizer("Hello", return_tensors="pt").to("cuda")

# Generate
with torch.no_grad():
    output = model.generate(**inputs, max_new_tokens=128)

print(tokenizer.decode(output[0]))

Trainium Code (PyTorch Native)

3 lines changed
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

# Load model
model = AutoModelForCausalLM.from_pretrained(
    "Qwen/Qwen3-8B",
    torch_dtype=torch.bfloat16,
)

# Move to device
model = model.to("neuron")           # ← just change device
model.eval()

# Compile for Neuron
model.forward = torch.compile(
    model.forward,
    backend="neuron",        # ← add backend
)

# Tokenize
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-8B")
inputs = tokenizer("Hello", return_tensors="pt").to("neuron")  # ← device

# Generate β€” IDENTICAL call
with torch.no_grad():
    output = model.generate(**inputs, max_new_tokens=128)

print(tokenizer.decode(output[0]))

Watch it end-to-end: our GPU training script β†’ Trainium

TorchTitan Qwen3-8B example β†—

Take a standard TorchTitan Qwen3-8B training run and move it to Trainium2 by applying a 4-line diff β€” no custom kernels, no model rewrite. Same torchrun … torchtitan.train command, different device.

Commands and the TorchTitan.diff shown are verbatim from the AWS Neuron repo. Training step metrics in this clip are illustrative for pacing, not a measured benchmark β€” see the Throughput tab for real, server-side numbers.

Neuron Explorer β€” Qwen3-8B Inference Profile

Perfetto-style timeline: Tensor Engine, Vector Engine, DMA, Memory β€” trn2.3xlarge (4 NeuronCores)
Open Full Screen β†—

☸️ vLLM on EKS β€” Live Cluster

Qwen3-8B served from a vLLM pod on a Trainium2 node (sa-east-1)
$ kubectl get nodes NAME STATUS ROLES VERSION ip-172-31-17-145.sa-east-1.compute.internal Ready <none> v1.32.0 $ kubectl get node -o jsonpath='{...allocatable}' aws.amazon.com/neuron: 1 aws.amazon.com/neuroncore: 8 $ kubectl get pods -l app=vllm-qwen3 -o wide NAME READY STATUS NODE vllm-qwen3-5bb9dbd7bf-scw59 1/1 Running ip-172-31-17-145 $ curl trn2-node:8001/v1/models served model: Qwen/Qwen3-8B (vLLM 0.16 + PyTorch Native on Neuron)

☸️ EKS Deployment Architecture

vLLM on Trainium with Kubernetes-native scaling
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Amazon EKS Cluster                    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚  Ingress / ALB    │───▢│   vLLM Service (ClusterIP)β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                    β”‚                     β”‚
β”‚           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”‚
β”‚           β–Ό                        β–Ό            β–Ό       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  vLLM Pod (TP=4) β”‚  β”‚  vLLM Pod (TP=4) β”‚  β”‚  ...   β”‚  β”‚
β”‚  β”‚  1 Trainium chip β”‚  β”‚  1 Trainium chip β”‚  β”‚        β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚        trn2 node            trn2 node                   β”‚
β”‚                                                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚  Neuron Device Plugin (DaemonSet)                 β”‚   β”‚
β”‚  β”‚  advertises aws.amazon.com/neuron to the schedulerβ”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚  HPA (scale replicas on latency / queue depth)   β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                                          β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ Observability Stack ──────────────────────
β”‚                                                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚ CloudWatch  β”‚  β”‚  OTel Agent  β”‚  β”‚ Neuron Monitor β”‚   β”‚
β”‚  β”‚ Container   β”‚  β”‚  (metrics +  β”‚  β”‚  (NeuronCore   β”‚   β”‚
β”‚  β”‚ Insights    β”‚  β”‚   traces)    β”‚  β”‚   utilization) β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    
Key Components (as deployed):
β€’ Neuron Device Plugin β€” DaemonSet that discovers NeuronCores and advertises aws.amazon.com/neuron as a schedulable resource
β€’ vLLM Deployment β€” pod requests aws.amazon.com/neuron: 1; scheduler places it on the trn2 node
β€’ Amazon VPC CNI + kube-proxy β€” pod networking and Service routing
β€’ neuron-monitor β†’ CloudWatch β€” per-core utilization, memory, execution stats (dashboard β†’)

Why it matters:
β€’ Same vLLM + PyTorch Native stack as standalone, now Kubernetes-scheduled βœ“
β€’ Trainium exposed as a native K8s resource βœ“
β€’ Scale by adding replicas / trn2 nodes βœ“

vLLM Deployment

Neuron device plugin β€” the manifest we deployed
# Neuron device plugin (DaemonSet) advertises NeuronCores
# kubectl apply -f k8s-neuron-device-plugin.yml

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-qwen3
spec:
  replicas: 1
  template:
    spec:
      nodeSelector:
        aws.amazon.com/neuron.present: "true"
      containers:
      - name: vllm
        image: public.ecr.aws/neuron/pytorch-inference-\
                 vllm-neuronx:0.16.0-sdk2.29.0
        args: ["python", "-m", "vllm.entrypoints.openai.api_server",
               "--model", "/models/Qwen3-8B",
               "--served-model-name", "Qwen/Qwen3-8B",
               "--tensor-parallel-size", "4",
               "--max-model-len", "512",
               "--dtype", "bfloat16"]
        resources:
          limits:
            aws.amazon.com/neuron: 1
        volumeMounts:
        - name: model
          mountPath: /models/Qwen3-8B
      volumes:
      - name: model
        hostPath: { path: /home/ubuntu/local-models/Qwen/Qwen3-8B }

Live CloudWatch Dashboard

neuron-monitor β†’ CloudWatch (auto-refresh every 10s)