Skip to content

Commit 3d2fef5

Browse files
committed
fix: update mlx-vlm pin and mtp batching
1 parent 0e8307e commit 3d2fef5

16 files changed

Lines changed: 64 additions & 757 deletions

omlx/engine/vlm.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,6 @@
3939
from ..api.utils import clean_special_tokens, detect_and_strip_partial
4040
from ..cache.vision_feature_cache import VisionFeatureSSDCache
4141
from ..models.vlm import VLMModelAdapter
42-
from ..patches.gated_delta_advance import apply_gated_delta_advance_patch
43-
from ..patches.qwen3_5_attention import apply_qwen3_5_attention_patch
4442
from ..utils.image import (
4543
compute_image_hash,
4644
compute_per_image_hashes,
@@ -748,15 +746,6 @@ def _load_vlm_sync():
748746
# and batched decode is fixed, so no separate mlx-lm decode model needed.
749747
self._adapter = VLMModelAdapter(self._vlm_model)
750748

751-
# Patch mlx-vlm GatedDeltaNet to mirror mlx-lm fixes (cache.advance(S)
752-
# + mx.contiguous on cache[0]) that mlx-vlm e41cd25 still lacks.
753-
# Class-level monkey-patch — no-op when target classes are absent
754-
# or already fixed upstream.
755-
apply_gated_delta_advance_patch()
756-
# Patch mlx-vlm Qwen3_5Attention to use plain RoPE on text-only
757-
# inputs. Preserves mRoPE for genuine multimodal positions.
758-
apply_qwen3_5_attention_patch()
759-
760749
# Create scheduler config
761750
scheduler_config = (
762751
copy.copy(self._scheduler_config)

omlx/models/vlm.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@
2525
import mlx.core as mx
2626
import mlx.nn as nn
2727

28-
from ..patches.qwen3_5_attention import force_text_only_rope
29-
3028
logger = logging.getLogger(__name__)
3129

3230

@@ -270,14 +268,9 @@ def __call__(
270268
position_ids = mx.broadcast_to(
271269
positions[None, :, None], (3, B, L)
272270
)
273-
# Decode never adds new image tokens; the broadcast
274-
# collapses the 3 mRoPE sections to identical values,
275-
# so the patched Qwen3_5Attention can skip its per-layer
276-
# .item() probes and take the plain-RoPE branch directly.
277-
with force_text_only_rope():
278-
result = self._language_model(
279-
input_ids, cache=cache, position_ids=position_ids, **kwargs
280-
)
271+
result = self._language_model(
272+
input_ids, cache=cache, position_ids=position_ids, **kwargs
273+
)
281274
else:
282275
result = self._language_model(
283276
input_ids, cache=cache, **kwargs
@@ -293,10 +286,9 @@ def __call__(
293286
position_ids = mx.broadcast_to(
294287
offsets[None, :, None], (3, B, L)
295288
)
296-
with force_text_only_rope():
297-
result = self._language_model(
298-
input_ids, cache=cache, position_ids=position_ids, **kwargs
299-
)
289+
result = self._language_model(
290+
input_ids, cache=cache, position_ids=position_ids, **kwargs
291+
)
300292
else:
301293
result = self._language_model(
302294
input_ids, cache=cache, **kwargs

omlx/patches/gated_delta_advance.py

Lines changed: 0 additions & 203 deletions
This file was deleted.

omlx/patches/mlx_lm_mtp/batch_generator.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ def patched_next(self, *args, **kwargs):
139139
_drop_mtp_state(self, "step-fallback")
140140
else:
141141
_drop_mtp_state(self, "non-singleton-or-ineligible")
142+
_mark_standard_multirow_decode(self)
142143
return original_next(self, *args, **kwargs)
143144

144145
def patched_extend(self, batch, *args, **kwargs):
@@ -246,9 +247,26 @@ def _mtp_common_eligible(gen_batch: Any) -> bool:
246247
def _allows_new_mtp_activation(gen_batch: Any, state_attr: str) -> bool:
247248
if getattr(gen_batch, state_attr, None) is not None:
248249
return True
250+
if getattr(gen_batch, "_omlx_mtp_saw_standard_multirow_decode", False):
251+
return False
249252
return bool(getattr(gen_batch, "_omlx_mtp_activation_safe", True))
250253

251254

255+
def _mark_standard_multirow_decode(gen_batch: Any) -> None:
256+
"""Remember that this batch has decoded with shared standard cache state.
257+
258+
A row that survives a standard multi-row decode and later becomes singleton
259+
no longer satisfies the narrow invariant that singleton MTP initialization
260+
relies on. Existing row-wise MTP state may continue, but starting a fresh
261+
singleton MTP state after late-join/late-finish reshaping is unsafe.
262+
"""
263+
try:
264+
if len(getattr(gen_batch, "uids", []) or []) > 1:
265+
gen_batch._omlx_mtp_saw_standard_multirow_decode = True
266+
except Exception:
267+
pass
268+
269+
252270
def _batch_rows_aligned_for_mtp(gen_batch: Any) -> bool:
253271
"""True when all batch rows share the same target-cache decode position."""
254272
prompt_cache = getattr(gen_batch, "prompt_cache", None) or []

omlx/patches/mlx_lm_mtp/qwen35_model.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
44
Adds an MTP head to ``mlx_lm.models.qwen3_5.TextModel`` (the language-model
55
half) and a pass-through on ``mlx_lm.models.qwen3_5.Model`` (the VLM-outer
6-
wrapper). The mechanism mirrors the patch idiom in
7-
``omlx/patches/gated_delta_advance.py``: replace class methods on a one-shot,
8-
idempotent basis tracked by a module flag.
6+
wrapper). The mechanism replaces class methods on a one-shot, idempotent
7+
basis tracked by a module flag.
98
109
Important: the class names below match what mlx-lm 0.31.x actually exports.
1110
Earlier drafts of this patch used ``Qwen3_5GatedDeltaNet`` / ``Qwen3_5DecoderLayer``
@@ -44,8 +43,7 @@
4443
find them.
4544
4645
The patch is intentionally limited to ``mlx_lm.models.qwen3_5``; mlx-vlm's
47-
``mlx_vlm.models.qwen3_5.language`` is a separate copy and is not touched
48-
(oMLX's existing ``gated_delta_advance.py`` already covers that side).
46+
``mlx_vlm.models.qwen3_5.language`` is a separate copy and is not touched.
4947
"""
5048

5149
from __future__ import annotations

omlx/patches/mlx_vlm_mtp/qwen35_moe_vlm_runtime.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,9 @@
2828
that an earlier iteration of this patch attempted.
2929
3030
Module-level apply ordering is significant: this patch must be applied
31-
*before* the model loads (so the patched ``__init__`` runs) and
32-
*before* ``omlx/patches/gated_delta_advance.py`` overrides
33-
``Qwen3_5GatedDeltaNet.__call__``. The current loader (``omlx/utils/model_loading.py``)
34-
calls ``apply_mlx_vlm_mtp_runtime_patch()`` in ``maybe_apply_pre_load_patches``
35-
which satisfies both.
31+
*before* the model loads so the patched ``__init__`` runs. The current loader
32+
(``omlx/utils/model_loading.py``) calls ``apply_mlx_vlm_mtp_runtime_patch()``
33+
in ``maybe_apply_pre_load_patches`` which satisfies that requirement.
3634
"""
3735

3836
from __future__ import annotations

omlx/patches/mlx_vlm_mtp/qwen35_vlm_runtime.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,9 @@
2626
and consumes the ``gdn_states`` returned from this patched ``__call__``.
2727
2828
Apply ordering: this patch must run *before* ``mlx_vlm.utils.load(...)``
29-
so the patched ``LanguageModel.__init__`` runs, and *before*
30-
``omlx/patches/gated_delta_advance.py`` overrides
31-
``Qwen3_5GatedDeltaNet.__call__``. ``maybe_apply_pre_load_patches`` in
29+
so the patched ``LanguageModel.__init__`` runs. ``maybe_apply_pre_load_patches`` in
3230
``omlx/utils/model_loading.py`` calls ``apply_mlx_vlm_mtp_runtime_patch``
33-
ahead of both, satisfying the ordering for inference. The oQ path in
31+
before loading the model, satisfying the ordering for inference. The oQ path in
3432
``omlx/oq.py:_measure_sensitivity`` also calls it before
3533
``vlm_load_model`` for sensitivity measurement.
3634
"""

0 commit comments

Comments
 (0)