LLM cold starts on HyperPod: from 27 minutes to seconds with model caching
Listen to article
generated on playGenerated only on first play
Powered by Amazon Polly + OmniVoice
SageMaker HyperPod now pre-loads model weights onto local NVMe and container images onto nodes, with roughly 60% faster scale-out on 57–145 GB models. I walk through migrating a document-analysis endpoint step by step — from the 27-minute time-to-first-token baseline to rewriting the autoscaling policy that only existed to live with the cold start.
An inference pod carrying a 145 GB model took more than 20 minutes between the scheduler accepting it and the first token coming out — and the autoscaler only asked for that pod once the queue was already full. This article tells the migration of an LLM endpoint on SageMaker HyperPod to the model caching AWS released on September 11, 2026: weights on local NVMe, pre-pulled image, and the part no announcement mentions — rewriting the autoscaling policy that had been tuned to live with the cold start.
The starting point: 27 minutes to first token
The endpoint that threads this story is a document-analysis pipeline — contracts and statements, bursty traffic at the start of business and at end-of-day close. A model with roughly 145 GB of weights on S3, vLLM in a custom 12 GB container on ECR, p5.48xlarge instances, an InferenceEndpointConfig with an autoScalingSpec triggered by CloudWatch invocations.
Measured from kubectl describe pod, the time between Scheduled and Ready split into three slices: 5 to 7 minutes pulling the image from ECR, more than 20 minutes downloading weights from S3 to the node disk, and about 90 seconds loading weights into HBM. The first two slices are network and have nothing to do with the model — they are the same download repeated on every pod, every scale-out, every node replacement by HyperPod's health check.
The operational consequence was not the time itself; it was what we did to live with it. We kept two permanent spare replicas, scaleUpStabilizationTime at zero and a low targetValue so the autoscaler would anticipate the burst. Two p5.48xlarge sitting idle 24 hours a day is 1,440 instance-hours per month bought only to hide a download. The right question was not "how do we make the pod start faster?" — it was "how much of the headroom exists only because the pod is slow?".
The journey, in the order it was done
- 1
Measure the baseline per slice
Before changing anything, we recorded per pod the image-pull time, the weight-download time and the GPU load time, from pod events and vLLM logs. Without that split there is no way to tell whether a gain came from the cache or from luck.
- 2
Confirm NVMe on the instance type
weightsCacherequires local NVMe at thehostPath(default/opt/dlami/nvme). An EBS-only instance does not warn you: the cache never warms and the pod silently falls back to S3. Thep5.48xlargeships 8 × 3.84 TB. - 3
Update the Inference Operator add-on
If
modelCacheConfigis not recognized, the operator is old. We updated the EKS add-on on a staging cluster first, because the same add-on bundles KEDA and the ALB controller — the update touches more than the cache. - 4
Enable only `imageCache`
First production change:
imageCache.enabled: true. It does not alter the inference pod, it only creates a DaemonSet that pre-pulls the image. Measured gain: the 5 to 7 minutes of pull became seconds, in line with the 97% AWS reports. - 5
Enable `weightsCache` and watch the labels
weightsCache.enabled: true, samehostPath. We watchedkubectl get nodes --show-labels | grep cache-readyuntil every node in the group was labeled before touching the autoscaler. - 6
Version the model path
The cache does not detect source changes. We started publishing weights under
s3://.../model/v2026-09-11/and changing thespecon every release — the path change is what forces a new warm-up. - 7
Rewrite the autoscaling
With scale-out in seconds, we removed the two spare replicas, raised
targetValueand keptscaleUpStabilizationTime: 0. The cold start stopped being a hidden parameter inside the policy.
Two caches, one node label and a preferred affinity
The design has three pieces worth understanding before trusting it.
Per-node warm-up: the operator downloads weights from the source (S3, FSx for Lustre, Hugging Face Hub or JumpStart) into a per-deployment isolated directory under the hostPath, on every node that satisfies the deployment's scheduling constraints. When it finishes, it applies a node label prefixed inference.sagemaker.aws.amazon.com/weights-cache-ready. followed by the configuration's UID; the image cache does the same with image-cache-ready. through a DaemonSet.
Preferred affinity, not required: the deployment uses preferred node affinity on those labels. A pod goes to a warm node when one exists; if none does, it is scheduled anyway and downloads from S3 and ECR as it always did. That is what makes the migration reversible: the worst case is the old behavior, never a pod stuck in Pending.
Read-only mount: the pod mounts the cache directory as a read-only hostPath volume and vLLM reads from local NVMe — AWS quotes about 7 GB/s — instead of crossing the network. The same 145 GB model that took more than 20 minutes to reach disk is now there before the pod exists.
What it does not do: it does not replicate cache across nodes, does not detect new weights under the same key, and does not survive node replacement. A node replaced by the health check is born cold, and the operator has to warm it again while the warm nodes keep serving.
Model caching lifecycle on HyperPod Inference
From spec apply to ready pod: who warms, who labels, where the fallback kicks in and why a fresh Karpenter node is still cold.
- Controller · reads InferenceEndpointConfig
- Weights warmer · per-node download
- Image DaemonSet · pre-pull per node
- S3 / FSx for Lustre · weights v2026-09-11
- ECR · vLLM image 12 GB
- Local NVMe · /opt/dlami/nvme (8 × 3.84 TB)
- Node label · cache-ready.<uid>
- vLLM pod · mounts cache read-only
- KEDA · autoScalingSpec (pods)
- Karpenter · new node = cold cache
What changes in autoscaling when the cold start disappears
Autoscaling on HyperPod has two layers, and the cache only acts on one of them.
Pod layer (KEDA): the autoScalingSpec reads CloudWatch or Amazon Managed Prometheus every pollingInterval (default 30 s; the HPA queries KEDA every 15 s) and decides replicas. With a 27-minute cold start, an honest policy has to fire early and keep headroom; that is why we had a conservative targetValue and idle replicas. With scale-out in seconds on a warm node, the same policy starts to oscillate — it brings up pods that are ready before the default 300 s metricCollectionPeriod window reflects the burst. We lowered metricCollectionPeriod to 60 s, raised targetValue and left scaleDownStabilizationTime at 300 s so capacity is not handed back on the first dip.
Node layer (Karpenter): the cache does not help here. A node Karpenter just provisioned has neither weights nor image; the first pod on it falls back and pays the full 27 minutes. If your peak requires new nodes, model caching shortens the second pod on that node, not the first. The decision we made was to keep a floor of warm GPU nodes — the cache warms on them outside peak hours — and use Karpenter only for the rare overflow.
Node-level scale-to-zero and model caching solve different problems. Choosing between them is choosing what you would rather pay for: an idle instance with a ready cache, or a full download in the busiest minute of the day.
Before and after, in the numbers AWS published and the ones we measured
The cost that stays: disk, version and node neighbors
Enabling the cache takes five lines of YAML. Keeping it is what costs, and three things became operating routine.
NVMe capacity: every cached deployment occupies its own directory on the same local disk of the node. Two 145 GB models plus a 57 GB one on a p5.48xlarge fit comfortably in 30 TB — but the team that consolidates six endpoints onto a smaller instance group discovers disk pressure only when warm-ups stop happening. The documentation is explicit: split instance groups or reduce cached deployments per node.
Version-based invalidation: since the source is not monitored, the S3 key is the version. Publishing new weights by overwriting latest/ leaves the warm node serving the old model indefinitely, with no error at all. A version suffix in the path and a fresh kubectl apply is the only reliable trigger; the operator cleans the old files when the old deployment is deleted.
Fallback signal: a pod that comes up without the hostPath volume is on the slow path and nobody gets told. We built a simple alert: count of nodes carrying the cache-ready label lower than the node count of the group for more than 10 minutes, plus per-pod Scheduled → Ready time exported to Prometheus. Without it, the regression to the old behavior goes unnoticed until the next peak — and the next peak is exactly when you do not want to find out.
The risks we managed along the way
Three silent failures concentrate the risk of this migration. Instance without NVMe at the hostPath: the cache never warms, nothing breaks and the cold start stays the same. Weights overwritten under the same key: warm nodes serve the old model until someone changes the spec. Fresh Karpenter node at peak: the first pod pays the full download; the cache only protects the second. None of the three raises an error — all of them produce latency that looks normal.
Anti-patterns I saw appear in the first week
- Enabling both caches and the new autoscaler in the same apply: when
Readytime drops and the policy oscillates on the same day, nobody knows which change caused what. One change at a time, against a measured baseline. - Treating the cache as a replica: it is per node, not per cluster. Consolidating deployments onto a small instance group to 'share' the cache only shares the disk — and its pressure.
- Node-level scale-to-zero with model caching as if they were complementary: when Karpenter removes the last node, it removes the cache with it. The first pod of the day is back to 27 minutes.
I would enable imageCache on every inference deployment on HyperPod at the very next change window — it is a DaemonSet, it does not touch the pod, and it hands back two minutes per scale-out asking nothing in return. weightsCache I only enable after confirming NVMe on the instance type and putting a version in the S3 path, because the two failures it introduces are mute. The lesson that stays from 16 years operating platforms: every cold-start optimization changes the autoscaling policy that was written to live with the old cold start — if you do not rewrite the policy, you keep paying the old headroom with the new problem already solved.
Verdict
Use HyperPod model caching when: the model exceeds 50 GB, traffic has predictable bursts, the fleet has local NVMe and you can keep a floor of warm nodes outside the peak. In that scenario it removes from the autoscaler the headroom that existed only to hide the download — and that is where the money shows up, not in the benchmark. Do not expect a gain when the peak requires fresh Karpenter nodes, when the fleet is EBS-only, or when the model fits in a pull of a few minutes: in those three cases the cost of maintaining path versioning and a fallback alert exceeds what the cache gives back. Enable imageCache always; enable weightsCache under the conditions above.
References
Architecture, AWS, AI and market deep dives — straight to your inbox. Free.
No spam · unsubscribe anytime
Ask Fernando about this
Get a focused answer about this article from my AI assistant, grounded in my work.
Join the conversation
Sign in to comment
Verify your email to join in — you'll also get the newsletter. No password.
Keep reading
Architecture intelligence, in your inbox
Curated signals and original analysis on AWS, AI, distributed systems and the market — the way a solutions architect reads them.
- Curated AWS · AI · architecture · market signals
- New architecture studies & deep-dives when they ship
- Sharp summaries — depth without the noise
- No spam · double opt-in · unsubscribe anytime