Files
vLLM-4x-RTX-3090/patches/0105-Offloader-Stream-every-multimodal-tower-parameter-to.patch
MOVZXandHinata fab0eb9fc7 feat: Offload the vision tower to host RAM
The vision tower sat on GPU 0 and 1 all the time, and did work only
when a request carried an image. It now streams from pinned host RAM
and encodes over H2D, which frees 432 MiB on GPU 0 and 1. Stage 1
carries no tower, so GPU 2 and 3 gain nothing, and the KV cache stays
at 924,993 tokens because --kv-cache-memory pins it. The freed memory
is headroom, not capacity. An image request passes: 2223 image tokens
in 11.77 s, with no VRAM growth.

Key the prebuilt ops cache on the compiled inputs, so a Python-only
patch installs from the shipped tarball instead of a 20-40 minute
compile.

Raise --mm-processor-cache-gb to 16. That cache is duplicated for each
API process and engine core process, so 16 GiB costs 32 GiB of host
RAM and no VRAM, and repeated images stop re-processing.

Co-Authored-By: Hinata <hinata@idihore.id>
Signed-off-by: MOVZX <movzx@yahoo.com>
2026-10-18 01:49:04 +07:00

171 lines
7.1 KiB
Diff

From 5441fcdcddfc11eb10ff1319a53e0613d6f46705 Mon Sep 17 00:00:00 2001
From: MOVZX <movzx@yahoo.com>
Date: Fri, 16 Oct 2026 16:07:53 +0700
Subject: [PATCH 105/105] [Offloader] Stream every multimodal tower parameter to host
RAM
Qwen3_VisionTransformer derived its compute device from
patch_embed.proj.weight and fed pos_embed.weight straight into the
position-embedding interpolation kernel, so the last two anchor weights
had to stay in VRAM for a tower to be streamable at all.
Record the device the tower was built on before the offloader moves
anything, and fall back to it when no weight is on the accelerator.
Move the interpolation table to that device per encode, which costs one
8.4 MiB host-to-device copy for a 456 MiB tower. Towers now leave no
parameters in VRAM between encodes; only small buffers such as the fused
input norm and rotary inv_freq remain.
Also let a tower restrict streaming through tower_offload_children, and
warn when a tower resolves its device property to another device type,
so a tower that reads an anchor weight reports the cause instead of a
Triton pointer error.
Co-authored-by: Hinata <hinata@idihore.id>
Signed-off-by: MOVZX <movzx@yahoo.com>
---
vllm/model_executor/models/qwen3_vl.py | 26 ++++++++++-
vllm/model_executor/offloader/prefetch.py | 54 ++++++++++++++++++-----
2 files changed, 66 insertions(+), 14 deletions(-)
diff --git a/vllm/model_executor/models/qwen3_vl.py b/vllm/model_executor/models/qwen3_vl.py
index 644fd13..b09e872 100644
--- a/vllm/model_executor/models/qwen3_vl.py
+++ b/vllm/model_executor/models/qwen3_vl.py
@@ -666,13 +666,25 @@ class Qwen3_VisionTransformer(nn.Module):
]
)
+ # An offloader moves tower parameters to host RAM after this
+ # constructor returns, and no weight is left on the accelerator to
+ # read the compute device from. Record it here while they are still
+ # placed, so `device` keeps working once every weight is streamed.
+ first_param = next(self.parameters(), None)
+ self._compute_device = (
+ torch.device("cpu") if first_param is None else first_param.device
+ )
+
@property
def dtype(self) -> torch.dtype:
return self.patch_embed.proj.weight.dtype
@property
def device(self) -> torch.device:
- return self.patch_embed.proj.weight.device
+ weight_device = self.patch_embed.proj.weight.device
+ if weight_device.type != "cpu":
+ return weight_device
+ return self._compute_device
def rot_pos_ids(self, h: int, w: int, spatial_merge_size: int) -> torch.Tensor:
cache_key = (h, w, spatial_merge_size)
@@ -739,7 +751,7 @@ class Qwen3_VisionTransformer(nn.Module):
for t, h, w in grid_thw:
outputs.append(
interpolate_fn(
- self.pos_embed.weight,
+ self.pos_embed_table(),
t,
h,
w,
@@ -750,6 +762,16 @@ class Qwen3_VisionTransformer(nn.Module):
)
return torch.cat(outputs, dim=0)
+ def pos_embed_table(self) -> torch.Tensor:
+ """The position embedding table on the compute device. The whole tower
+ may be streamed from host RAM, but the interpolation kernel cannot read
+ host memory.
+ """
+ table = self.pos_embed.weight
+ if table.device.type == "cpu":
+ table = table.to(self.device, non_blocking=True)
+ return table
+
def prepare_encoder_metadata(
self,
grid_thw_list: list[list[int]],
diff --git a/vllm/model_executor/offloader/prefetch.py b/vllm/model_executor/offloader/prefetch.py
index f276673..cd70138 100644
--- a/vllm/model_executor/offloader/prefetch.py
+++ b/vllm/model_executor/offloader/prefetch.py
@@ -251,6 +251,12 @@ class PrefetchOffloader(BaseOffloader):
def _wrap_tower(self, tower: nn.Module, prefix: str):
"""Split a tower into parameter-bearing units and place each one
in pinned host RAM until its forward executes (bulk-copy).
+
+
+ A tower restricts this to its heavy compute subtrees by declaring
+ `tower_offload_children`. Tower code reads a small anchor weight to find
+ its compute device, and passes embedding weights straight to a kernel,
+ so weights outside that list stay on the device.
"""
first_param = next(tower.parameters(), None)
if first_param is None:
@@ -261,26 +267,50 @@ class PrefetchOffloader(BaseOffloader):
if device.type == "cpu":
return
+ declared = getattr(tower, "tower_offload_children", None)
+ children = dict(tower.named_children())
+ if declared is None:
+ roots: list[nn.Module] = [tower]
+ kept: list[str] = []
+ else:
+ roots = [c for name, c in children.items() if name in declared]
+ kept = [name for name in children if name not in declared]
+
num_units = 0
- for submodule in tower.modules():
- param_names = [
- name for name, _ in submodule.named_parameters(recurse=False)
- ]
- if not param_names:
- continue
- streamer = _TowerUnitStreamer(submodule, param_names, device)
- streamer.offload_to_host_now()
- self.tower_streamers.append(streamer)
- num_units += 1
+ for root in roots:
+ for submodule in root.modules():
+ param_names = [
+ name for name, _ in submodule.named_parameters(recurse=False)
+ ]
+ if not param_names:
+ continue
+ streamer = _TowerUnitStreamer(submodule, param_names, device)
+ streamer.offload_to_host_now()
+ self.tower_streamers.append(streamer)
+ num_units += 1
logger.info(
"[PrefetchOffloader] Queued tower '%s' for host streaming: "
- "%d parameter units (weights move to the GPU only during "
- "encoder forwards).",
+ "%d parameter units, %d children kept on device (%s) (weights "
+ "move to the GPU only during encoder forwards).",
prefix,
num_units,
+ len(kept),
+ ", ".join(kept) or "none",
)
+ if isinstance(getattr(type(tower), "device", None), property):
+ resolved = getattr(tower, "device", None)
+ if isinstance(resolved, torch.device) and resolved.type != device.type:
+ logger.warning(
+ "[PrefetchOffloader] Tower '%s' resolves its compute "
+ "device to %s from offloaded weights, so code that calls "
+ ".to(tower.device) runs on the host. Add the anchor "
+ "submodule to tower_offload_children.",
+ prefix,
+ resolved,
+ )
+
def _hook_module_forward(self, index: int, module: nn.Module):
"""Hook module's forward with torch.compile-compatible sync."""
original_forward = module.forward
--
2.47.3