Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions tensorrt_llm/_torch/model_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,13 @@ class ModelConfig(Generic[TConfig]):
# If true, ONLY the vision encoder part of the full model is loaded/executed.
mm_encoder_only: bool = False

# If true, the multimodal encoder of a multimodal checkpoint is NOT
# instantiated/loaded and the model serves text-only requests. This is
# opt-in per model: each model implementation must honor this flag when
# building its encoder (currently the Qwen3-VL / Qwen3.5-VL models); a
# model that does not check it simply ignores the flag (no-op).
disable_mm_encoder: bool = False

# Video pruning rate for VLM models (None = EVS disabled)
video_pruning_rate: Optional[float] = None

Expand Down
4 changes: 2 additions & 2 deletions tensorrt_llm/_torch/models/modeling_qwen3_5.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
from ..pyexecutor.config_utils import get_qwen3_hybrid_layer_types
from .checkpoints.base_weight_mapper import BaseWeightMapper
from .checkpoints.hf.qwen3_5_weight_mapper import Qwen3_5MoeHfWeightMapper
from .modeling_multimodal_utils import _is_mm_disagg
from .modeling_qwen3_next import Qwen3NextForCausalLM
from .modeling_qwen3vl import (
Qwen3VisionModel,
Expand Down Expand Up @@ -691,7 +690,8 @@ def multimodal_data_device_paths(self) -> List[str]:
]

def load_weights(self, weights: Dict[str, torch.Tensor], weight_mapper: BaseWeightMapper):
if not _is_mm_disagg():
# None under MM E/P disagg or disable_mm_encoder.
if self.mm_encoder is not None:
self.mm_encoder.load_weights(weights)

weight_mapper = Qwen3_5MoeHfWeightMapper()
Expand Down
11 changes: 9 additions & 2 deletions tensorrt_llm/_torch/models/modeling_qwen3vl.py
Original file line number Diff line number Diff line change
Expand Up @@ -1253,11 +1253,18 @@ def __init__(
self.llm = AutoModelForCausalLM.from_config(llm_model_config)

self.mm_encoder = None
# Normal workers own the encoder. MM E/P handoff uses attached embeddings.
if not _is_mm_disagg():
# Normal workers own the encoder. MM E/P handoff uses attached
# embeddings; disable_mm_encoder serves the checkpoint text-only and
# saves the encoder's GPU memory for the KV cache pool.
if not (_is_mm_disagg() or model_config.disable_mm_encoder):
self.mm_encoder = Qwen3VisionModelBase(
copy.deepcopy(model_config), kwargs.get("vision_model_class", None)
).eval()
elif model_config.disable_mm_encoder:
logger.info(
f"{type(self).__name__}: multimodal encoder disabled "
"(disable_mm_encoder=True); serving text-only requests."
)

self.use_deepstack = hasattr(config.vision_config, "deepstack_visual_indexes")
self.deepstack_num_level = (
Expand Down
4 changes: 4 additions & 0 deletions tensorrt_llm/_torch/pyexecutor/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,10 @@ def _create_dummy_encoder_inputs(self) -> List[MultimodalParams]:
self._profiling_stage_data,
dict) and not self._profiling_stage_data.get("enable_mm_reqs"):
return []
# No local multimodal encoder (disable_mm_encoder or MM E/P disagg):
# nothing to profile.
if getattr(self._model_engine.model, "mm_encoder", object()) is None:
Comment thread
2ez4bz marked this conversation as resolved.
return []
input_processor = self._model_engine.input_processor
_, encoder_max_num_tokens = self._llm_args.get_encoder_runtime_sizes()
# Modality-agnostic: the model declares each modality's per-item token
Expand Down
1 change: 1 addition & 0 deletions tensorrt_llm/_torch/pyexecutor/model_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -1184,6 +1184,7 @@ def _load_and_validate_config(
lora_config=self.lora_config,
allreduce_strategy=self.llm_args.allreduce_strategy,
mm_encoder_only=self.llm_args.mm_encoder_only,
disable_mm_encoder=self.llm_args.disable_mm_encoder,
attn_backend=self.llm_args.attn_backend,
moe_backend=self.llm_args.moe_config.backend,
moe_disable_finalize_fusion=self.llm_args.moe_config.
Expand Down
10 changes: 10 additions & 0 deletions tensorrt_llm/bench/benchmark/throughput.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,16 @@ def throughput_command(
kwargs = kwargs | runtime_config.get_llm_args()
kwargs['skip_tokenizer_init'] = not no_skip_tokenizer_init
kwargs['backend'] = options.backend
if (options.modality is None and options.backend == "pytorch"
and "disable_mm_encoder" not in kwargs
and not kwargs.get("mm_encoder_only", False)):
Comment thread
thorjohnsen marked this conversation as resolved.
# Text-only benchmark: skip a multimodal checkpoint's encoder so
# its GPU memory goes to the KV cache pool instead. Text-only
# models ignore this flag. Overridable via extra_llm_api_options.
kwargs["disable_mm_encoder"] = True
logger.info(
"Text-only benchmark (--modality not set): the multimodal "
"encoder, if the model has one, will not be loaded.")
if bench_env.telemetry_config is not None:
kwargs["telemetry_config"] = bench_env.telemetry_config

Expand Down
17 changes: 17 additions & 0 deletions tensorrt_llm/llmapi/llm_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -5097,6 +5097,18 @@ def validate_encoder_runtime_sizes(cls, v: Optional[int]) -> Optional[int]:
"Only load/execute the vision encoder part of the full model. Defaults to False.",
status="prototype")

disable_mm_encoder: bool = Field(
default=False,
description=
"Skip instantiating and loading the multimodal (e.g. vision) encoder "
"of a multimodal checkpoint and serve it text-only. Saves the "
"encoder's GPU memory (enlarging the KV cache pool) for workloads "
"that never send image/video/audio inputs; such requests are "
"rejected. Only takes effect for model implementations that support "
"it (currently the Qwen3-VL / Qwen3.5-VL family); a no-op otherwise. "
"Defaults to False.",
status="prototype")

encode_only: bool = Field(
default=False,
description=
Expand Down Expand Up @@ -5272,6 +5284,11 @@ def validate_encoder_modes(self) -> 'TorchLlmArgs':
"Use encode_only=True for LLM.encode(), or use "
"MultimodalEncoder/mm_encoder_only for multimodal encoder "
"execution.")
if self.disable_mm_encoder and self.mm_encoder_only:
raise ValueError(
"disable_mm_encoder and mm_encoder_only are mutually "
"exclusive: one skips the multimodal encoder, the other runs "
"only the multimodal encoder.")
return self

@model_validator(mode="after")
Expand Down
7 changes: 7 additions & 0 deletions tensorrt_llm/usage/llm_args_golden_manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,13 @@
"kind": "value",
"path": "cuda_graph_config.seq_lens"
},
{
"allowed_values": [],
"annotation": "<class 'bool'>",
"converter": "",
"kind": "value",
"path": "disable_mm_encoder"
},
{
"allowed_values": [],
"annotation": "<class 'bool'>",
Expand Down
25 changes: 25 additions & 0 deletions tests/unittest/_torch/modeling/test_modeling_qwen3_5_vl.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,31 @@ def test_qwen35_dense_vl_resolves_model_and_mapper(tmp_path: Path) -> None:
)


def test_qwen35_dense_vl_disable_mm_encoder_skips_vision_tower(
tmp_path: Path,
) -> None:
"""disable_mm_encoder must not instantiate the vision encoder (nvbug
6405760: the tower's GPU memory shrank the KV cache pool for text-only
serving of VLM checkpoints)."""
config = load_pretrained_config(str(_write_qwen35_dense_vl_config(tmp_path)))
model_config = ModelConfig(pretrained_config=config, disable_mm_encoder=True)
with torch.device("cuda"):
model = Qwen3_5VLModel(model_config)

assert model.mm_encoder is None
assert not any("mm_encoder" in name for name, _ in model.named_parameters())


def test_qwen35_dense_vl_default_keeps_vision_tower(tmp_path: Path) -> None:
config = load_pretrained_config(str(_write_qwen35_dense_vl_config(tmp_path)))
model_config = ModelConfig(pretrained_config=config)
with torch.device("cuda"):
model = Qwen3_5VLModel(model_config)

assert model.mm_encoder is not None
assert any("mm_encoder" in name for name, _ in model.named_parameters())


Comment thread
thorjohnsen marked this conversation as resolved.
def test_qwen35_dense_vl_placeholder_metadata_registered() -> None:
metadata = MULTIMODAL_PLACEHOLDER_REGISTRY.get_placeholder_metadata("qwen3_5")

Expand Down
4 changes: 4 additions & 0 deletions tests/unittest/api_stability/references/llm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ methods:
annotation: bool
default: False
status: prototype
disable_mm_encoder:
annotation: bool
default: False
status: prototype
encode_only:
annotation: bool
default: False
Expand Down
Loading