Dev.to AI πŸ€– Ai πŸ‘ 0 πŸ“– 10 min read

vLLM 0.33 Ships an ABI-Stable Wheel, but PyPI Still Pins torch==2.14.0: What You Can Actually Swap

TL;DR On November 6 the vLLM team, with the Red Hat and Meta PyTorch teams, published how vLLM's CUDA build moved to the LibTorch Stable ABI. Since v0.33.0 (PyPI upload 2026-11-02), the CUDA wheel is meant to keep work

TL;DR

On November 6 the vLLM team, with the Red Hat and Meta PyTorch teams, published how vLLM's CUDA build moved to the LibTorch Stable ABI. Since v0.33.0 (PyPI upload 2026-11-02), the CUDA wheel is meant to keep working when you replace the torch wheel with a newer compatible one. I checked what that means in practice.

  • I reproduced the audit. I downloaded the 336 MB x86_64 wheel from PyPI and ran torch-abi-audit 0.4.0 on it. Result: Torch ABI: STABLE, Min torch: 2.13.0, 13 extensions, 0 unstable symbols. It matches the blog post's output line for line.
  • One extension sets the floor. Three extensions need torch 2.10 or newer, six need 2.11, and one (QuTLASS) needs 2.13. The wheel's floor is the highest of those.
  • The package metadata did not follow. vLLM 0.33.0 on PyPI still declares torch==2.14.0 and torchvision==0.29.0. A binary that can run on newer torch is not the same as a package that lets pip install it.
  • The old rebuild tax was real. Across four torch releases, vLLM took 33, 34, 44 and 60 days to ship the first release that pins each one. torch 2.12.0 was never pinned at all.
  • What I did not do. I ran no GPU test and did not swap torch under a running server. The blog's own demo is one 135M model generating 8 tokens in eager mode.

What did vLLM actually change in 0.33?

vLLM's C++ and CUDA code calls into LibTorch constantly. Before this work, a change to LibTorch's binary interface could force a vLLM rebuild even when the Python-visible API was identical. The post describes the fix as three layers of PyTorch's stable ABI: a stable C shim exposing selected LibTorch functions, C++ wrappers over that shim, and header-only utilities in torch::headeronly that do not link against libtorch.so.

The scale of the migration, from the post:

  • Over 175 .cpp and .cu files plus 120 or more headers in the CUDA build.
  • Seven compiled dependencies that also use LibTorch: FlashMLA, DeepGEMM, FlashAttention-2, FlashAttention-3, DeepSelect, FlashKDA and QuTLASS. Several were migrated on vLLM's own forks first, with upstream pull requests still open for some.
  • A timeline from an RFC on 2025-10-15 and a first migration PR on 2025-12-29, through in-tree code becoming stable in June 2026, to DeepGEMM on 2026-10-15, which was the last piece.

Two mechanics are worth copying if you ship your own CUDA ops. The TORCH_TARGET_VERSION build flag makes compilation fail if any unstable LibTorch header sneaks in, and ops are registered with STABLE_TORCH_LIBRARY with the implementation wrapped in TORCH_BOX, which boxes and unboxes arguments into StableIValue.

The post is also open about the limits. The stable surface is a subset of LibTorch: there is no stable SymInt, so meta functions had to be Python, and no autograd machinery, so backward functions must be Python too. A method as ordinary as is_pinned() had no stable equivalent during the migration, so vLLM calls aten::is_pinned through the generic torch_call_dispatcher shim. PyTorch added a stable is_pinned() in 2.14.

What does the audit say about the wheel you can download?

The blog shows torch-abi-audit output, but the question I care about is whether the artifact on PyPI matches it. I unpacked vllm-0.33.0-cp38-abi3-manylinux_2_28_x86_64.whl and audited the 13 extension modules inside. I did not audit the aarch64 wheel.

Per-extension stable shim symbol counts in the vLLM 0.33.0 wheel, colored by the minimum torch version each one needs

Extension Stable shim symbols Min torch
_C_stable_libtorch 88 2.11.0
deep_gemm/_C_extension 85 2.11.0
_moe_C_stable_libtorch 72 2.11.0
vllm_flash_attn/_vllm_fa2_C 69 2.10.0
_flashmla_C 66 2.11.0
_flashmla_extension_C 63 2.11.0
vllm_flash_attn/_vllm_fa3_C 63 2.10.0
_qutlass_C 63 2.13.0
_flashkda_C 43 2.11.0
_deepselect_C 40 2.10.0

Three more modules (cumem_allocator, fs_io_C, spinloop) do not touch torch at all. Every torch-linked module reported unstable=0.

The tool reports "the minimum LibTorch version that provides all the required stable symbols", so the wheel-level answer of 2.13.0 is the maximum over the table: one extension, QuTLASS, raises the floor by two minor versions over most of the rest. If you ever wonder why a wheel built on a stable ABI still refuses an older torch, this is the place to look.

Unstable libtorch symbols per bundled library before and after the dependency migration

The blog's earlier audit, taken before the seven dependencies were migrated, listed six libraries with unstable symbols: 85 (FlashAttention-2), 81 (FlashAttention-3), 70 and 68 (the two FlashMLA modules), and 59 each for QuTLASS and DeepGEMM. That is 422 unstable symbol uses in total, and the shipped wheel has none.

Two gotchas from running the tool myself. It shells out to nm, so on Windows a single .so fails with 'nm' not found. Pointed at the unpacked directory, it printed Torch ABI: NO-TORCH and Extensions: 0, presumably because Windows Python lists only .pyd as extension-module suffixes, so .so files are skipped. That is a false clean bill of health for a perfectly good wheel. I got the real result by running it in WSL Ubuntu, where nm exists.

If the binary is stable, why can't I just pip install a newer torch?

Because the stable ABI fixes binary compatibility and says nothing about what the package declares. The metadata I read from PyPI on 2026-11-08:

  • vllm==0.33.0 requires torch==2.14.0, torchvision==0.29.0 and flashinfer-python==0.7.2.post1.
  • torchvision==0.29.0 itself requires torch==2.14.0.
  • torchvision 0.29.1 (2026-09-30) and 0.29.2 (2026-10-28) relaxed that to torch>=2.14.0.

So the PyTorch side has already moved from exact pins to lower bounds for torchvision, while vLLM's own metadata still carries exact ones. The 0.33.0 pin of 2.14.0 is also one minor version above the audited floor of 2.13.0, and the post does not say why.

The post's demo sidesteps this. It builds vLLM from source (the printed version is 0.1.dev24716+gcb95d994f.d20261105, not a release), runs it on torch 2.14.1+cu132, swaps in torch==2.15.0 and reruns without rebuilding. Both runs print the same text for the prompt "The capital of France is". That is a useful existence proof, with limits worth stating:

  • The model is SmolLM2-135M-Instruct, max_model_len=128, 8 generated tokens at temperature 0.
  • It runs with enforce_eager=True, which turns CUDA Graphs off. That is the mode where the post says the shim's overhead can matter, and no timing is reported.
  • It sets VLLM_USE_FLASHINFER_SAMPLER=0 without explaining why. FlashInfer is a separate PyPI package that vLLM pins, and it is not one of the seven dependencies in the post's migration table, so the audit says nothing about it.

How long did the old model make you wait?

The rebuild tax shows up as a delay between a torch release and the first vLLM release that pins it. I measured it from PyPI upload timestamps, using the torch requirement in each release's metadata.

Days between each torch release and the first vLLM release that pins it

The script that produced the numbers, so you can rerun it later and see whether the 0.33 line changes the pattern:

import json, re, urllib.request
from datetime import datetime

def pypi(name, version=None):
    url = f"https://pypi.org/pypi/{name}/{version + '/' if version else ''}json"
    with urllib.request.urlopen(url, timeout=60) as r:
        return json.load(r)

def first_upload(files):
    return datetime.fromisoformat(min(f["upload_time"] for f in files))

def pin(info, pkg):
    for req in info.get("requires_dist") or []:
        m = re.match(rf"{pkg}\s*(==|>=)\s*([\w.]+)", req)
        if m:
            return m.group(1) + m.group(2)

torch_dates = {v: first_upload(f) for v, f in pypi("torch")["releases"].items() if f}
vllm = pypi("vllm")["releases"]
seen = {}
for v in sorted((v for v in vllm if re.fullmatch(r"0\.\d+\.\d+", v) and vllm[v]),
                key=lambda s: tuple(map(int, s.split(".")))):
    if tuple(map(int, v.split("."))) < (0, 17, 0):
        continue
    p = pin(pypi("vllm", v)["info"], "torch")
    if p and p not in seen:
        seen[p] = (v, first_upload(vllm[v]))
for p, (v, when) in seen.items():
    t = p.lstrip("=>")
    print(f"vllm {v:7} first pins torch{p:9} | torch released {torch_dates[t]:%Y-%m-%d}"
          f" | vllm shipped {when:%Y-%m-%d} | lag {(when - torch_dates[t]).days} days")

cur = pypi("vllm", "0.33.0")["info"]
print("\nvllm 0.33.0 metadata:", {k: pin(cur, k) for k in ("torch", "torchvision", "flashinfer-python")})
for tv in ("0.29.0", "0.29.1", "0.29.2"):
    print(f"torchvision {tv} requires torch{pin(pypi('torchvision', tv)['info'], 'torch')}")

Output from running it today:

vllm 0.17.0  first pins torch==2.10.0  | torch released 2026-01-21 | vllm shipped 2026-03-07 | lag 44 days
vllm 0.20.0  first pins torch==2.11.0  | torch released 2026-03-23 | vllm shipped 2026-04-27 | lag 34 days
vllm 0.27.0  first pins torch==2.13.0  | torch released 2026-07-08 | vllm shipped 2026-08-10 | lag 33 days
vllm 0.33.0  first pins torch==2.14.0  | torch released 2026-09-02 | vllm shipped 2026-11-02 | lag 60 days

vllm 0.33.0 metadata: {'torch': '==2.14.0', 'torchvision': '==0.29.0', 'flashinfer-python': '==0.7.2.post1'}
torchvision 0.29.0 requires torch==2.14.0
torchvision 0.29.1 requires torch>=2.14.0
torchvision 0.29.2 requires torch>=2.14.0

Three things stand out in that data.

First, the lag was 33 to 60 days, with the longest one being the most recent: torch 2.14.0 came out on 2026-09-02 and vLLM first pinned it on 2026-11-02. The median of the four is 39 days.

Second, torch 2.12.0 (2026-05-13) was never pinned by any release from 0.17.0 onward. vLLM went straight from 2.11.0 to 2.13.0 at 0.27.0.

Third, a pin sticks. The 2.11.0 pin covered 11 releases over 105 days, and the 2.13.0 pin covered 7 releases over 84 days. For an operator, "which torch does the current vLLM release want" has had a stable answer for months at a time, and that is the thing the stable ABI is meant to loosen.

A caveat on interpretation: PyPI pins are a proxy for what the default wheel supported. The main branch and nightlies may have worked with a new torch earlier, and I did not check. torch 2.15.0 went up on 2026-10-28, so it is 11 days old as of this writing, and vLLM 0.33.0 is still on 2.14.0. If the median lag held, a pin would land around December 6. That is a simple extrapolation from four points, not a prediction.

What should an operator do with this today?

My recommendation is to treat the pin as a tested combination and the ABI as headroom, not as a promise. A concrete approach:

  1. Stay on the pinned torch for production until you have a reason not to. The audit tells you symbols resolve. It does not tell you that kernels behave the same on a new CUDA or driver combination, and the post says plainly that Python APIs, the minimum CUDA version and third-party dependencies still constrain compatibility.
  2. If you need a newer torch, do it in staging and expect pip to complain. Installing a newer torch on top of vLLM 0.33.0 should make pip print its incompatible-requirements warning about the exact pins, and a fresh pip install vllm torch==2.15.0 should fail resolution. I have not run this swap. If you use uv, its --override option replaces a dependency's declared requirement, which is the cleaner way to say "I know".
  3. Audit what you actually deploy. Run the check against the installed package, not against the blog post's version of events:
pip install torch-abi-audit            # needs nm (binutils) on PATH
torch-abi-audit --check both vllm      # exit 1 on a violation, 2 on an operational error

Per the tool's --help, --check torch fails on libtorch use without the stable ABI, --check abi3 fails when a torch-using package is not CPython abi3 compliant, and both requires both. Add it to the image build for your own custom ops too.

  1. Smoke test with CUDA Graphs on. The one overhead the post flags (type conversion and dispatcher boxing in the shim) is host-side and is claimed to vanish after warmup under CUDA Graphs. Verify that with your own eager and graph-mode latency, because the post publishes no numbers.
  2. Know which builds are covered. The post says vLLM publishes ABI stable CUDA wheels as of 0.33.0 and lists CPU and ROCm as future work. ROCm shares most sources with CUDA through hipify, with only a few ROCm-specific files left.

FAQ

Can I upgrade PyTorch under an installed vLLM 0.33.0 without rebuilding?
At the binary level the audit says the wheel needs symbols that exist from torch 2.13.0, and the post demonstrates 2.14.1 to 2.15.0 on a source build. At the package level, PyPI metadata pins torch==2.14.0, so you would be overriding the pin. I have not tested the swap on a GPU.

Why is the wheel's minimum torch 2.13.0 when most extensions need 2.10 or 2.11?
The reported floor is the highest requirement of any extension, and _qutlass_C requires 2.13.0. The audit output shows this directly.

Does the stable ABI make vLLM slower?
The post says the shim adds type-conversion overhead, plus operator lookup and boxing for wrappers that go through torch_call_dispatcher. It calls this usually negligible and gone after warmup under CUDA Graphs, but possibly visible in eager mode with very fast kernels, and reports no noticeable regression. No measurements are published.

Does it cover CPU and ROCm builds?
Not yet. The announcement covers the CUDA wheels from 0.33.0. CPU and ROCm are listed as next steps.

Does a stable ABI remove CUDA version constraints?
No. The post states that Python APIs, the minimum supported CUDA version and third-party dependencies still affect compatibility. The ABI only addresses binary compatibility with LibTorch.

How do I check any other wheel for ABI stability?
Install torch-abi-audit, make sure nm is available, and run it on the package or a .so path. The --json flag gives machine-readable output for CI.

Further reading

Sources

πŸ“° Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.