Skip to content

fix: reset model-owned VLM fallback state - #34

Merged
inureyes merged 1 commit into
mainfrom
fix/reset-vlm-fallback-state
May 19, 2026
Merged

inureyes merged 1 commit into
mainfrom
fix/reset-vlm-fallback-state

Conversation

@inureyes

Copy link
Copy Markdown
Member

Summary

VLM single-row generation paths (CLI and bench invocations that do not carry a SequenceId) were leaking fallback cache state between runs. CxxGenerator::generate_streaming{,_with_embeddings} and generate_with_stats{,_and_embeddings} only called self.reset(), which clears the generator-owned KVCache vector but never touches the model-owned fallback slot inside ModelOwnedSequenceState. A second VLM prefill then reused offsets from the previous run against a freshly built 4D attention mask, producing wrong outputs or panicking when the cached position fell outside the new mask.

Fix

Introduce a new LanguageModel::reset_runtime_state() trait hook with a default no-op body (correct for models whose entire state lives in the caller's KVCache slice) and call it from CxxGenerator::reset_with_model. Switch every single-row generation path off the bare reset() and onto reset_with_model(model) so the generator-owned caches and the model-owned fallback caches are cleared in lockstep. Per-sequence scheduler entries are intentionally untouched — only the legacy fallback slot used by SequenceId-less paths is reset.

Per-Model Implementations

  • Gemma 3, Gemma 4, Llama 4 — forward the hook to their existing reset_caches() routines.
  • Qwen 3.5 Next — calls sequence_state.replace_internal(make_internal_caches()) directly so the VLM-populated MRoPE fallback slot is preserved; VLM callers write that slot during embedding preparation immediately before generation begins.
  • Gemma4VLModel / Qwen35VLModel — delegate to the text backbone's hook.
  • VisionLanguageModel / LoadedModel — propagate the new hook through their existing delegate_language_model! machinery so every wrapper layer participates.

Test plan

  • cargo fmt --check
  • cargo check --tests --features metal,accelerate
  • cargo clippy --features metal,accelerate --all-targets -- -D warnings
  • reset_with_model_invokes_runtime_state_hook unit test (uses a tracking model to guarantee reset_with_model actually invokes the new hook, so future single-row paths cannot silently regress past it)
  • tests/vlm_concurrency.rs — ignored, weight-gated smoke test driving two concurrent image chat requests against a real mlxcel-server Gemma 3n VLM instance; run locally with cargo test --release --test vlm_concurrency -- --ignored when Gemma 3n VLM weights are available

VLM single-row generation paths (CLI and `bench` invocations that do not carry a `SequenceId`) were leaking fallback cache state between runs. `CxxGenerator::generate_streaming{,_with_embeddings}` and `generate_with_stats{,_and_embeddings}` only called `self.reset()`, which clears the generator-owned `KVCache` vector but never touches the model-owned fallback slot inside `ModelOwnedSequenceState`. A second VLM prefill then reused offsets from the previous run against a freshly built 4D attention mask, producing wrong outputs or panicking when the cached position fell outside the new mask.

Introduce a new `LanguageModel::reset_runtime_state()` trait hook with a default no-op body (correct for models whose entire state lives in the caller's `KVCache` slice) and call it from `CxxGenerator::reset_with_model`. Switch every single-row generation path off the bare `reset()` and onto `reset_with_model(model)` so the generator-owned caches and the model-owned fallback caches are cleared in lockstep. Per-sequence scheduler entries are intentionally untouched — only the legacy fallback slot used by `SequenceId`-less paths is reset.

Per-model implementations: Gemma 3, Gemma 4, and Llama 4 forward the hook to their existing `reset_caches()` routines. Qwen 3.5 Next instead calls `sequence_state.replace_internal(make_internal_caches())` directly so the VLM-populated MRoPE fallback slot is preserved — VLM callers write that slot during embedding preparation immediately before generation begins. `Gemma4VLModel` and `Qwen35VLModel` delegate the hook to their text backbones, and both `VisionLanguageModel` and `LoadedModel` propagate it through their existing `delegate_language_model!` machinery so every wrapper layer participates.

Add `tests/vlm_concurrency.rs` — an ignored, weight-gated smoke test that drives two concurrent image chat requests against a real `mlxcel-server` Gemma 3n VLM instance and asserts both responses come back without cross-contamination. Add a `reset_with_model_invokes_runtime_state_hook` unit test in `mlxcel-core` that uses a tracking model to assert `reset_with_model` actually invokes the new hook, so future single-row paths cannot silently regress past it.
@inureyes inureyes added type:bug Bug fixes, error corrections, or issue resolutions priority:high High priority area:models Model architectures, weights, loading, metadata area:core mlxcel-core: MLX FFI, primitives, KV cache, layers labels May 19, 2026
@inureyes
inureyes merged commit fdf1e56 into main May 19, 2026
@inureyes
inureyes deleted the fix/reset-vlm-fallback-state branch May 19, 2026 14:36
inureyes added a commit that referenced this pull request May 19, 2026
Bump version to 0.0.29. Highlights: model-owned VLM fallback state reset (#34), Gemma 3n VLM padded prefill alignment plus the new mlxcel-bench-decode same-process benchmark harness (#36), and benchmark report reorganization with refreshed Apple Silicon and M5 Max VLM tables (#32, #33).
inureyes added a commit that referenced this pull request May 20, 2026
`qwen2.5-vl-3b-4bit` was marked FAIL:warmup in the M5 Max benchmark sweep. The failure surfaced in the same-process bench harness: a warmup pass left stale generator-owned KV state, and the subsequent measured pass broadcast a 4-D VLM attention mask against those stale keys and aborted. PR #34 (`fix: reset model-owned VLM fallback state`) fixed this transitively by rebuilding generator-owned KV caches before each single-row run in `CxxGenerator::reset_with_model()`; `qwen2_5_vl` carries no model-owned fallback state, so no additional production code change is needed here.

This commit adds the regression test `qwen2_5_vl_single_row_bench_prefill_succeeds_with_m5_warmup` in `tests/vlm_concurrency.rs`, which drives the exact FAIL:warmup repro path — an image VLM run with `--warmup-tokens 20` so the harness performs both a warmup pass and a measured pass in the same process. It mirrors the existing `gemma3n_vlm_single_row_bench_prefill_accepts_m5_tile_padding` guard and is `#[ignore]`d behind local Qwen2.5-VL weights and the `mlxcel-bench-decode` binary.

The M5 Max benchmark doc (`docs/benchmark_results/model_tests_m5max.md`) is updated to reflect the now-passing run: text prefill 1529.43 tok/s / decode 165.11 tok/s (EOS at 39 tokens) and VLM prefill 1696.53 tok/s / decode 156.83 tok/s (EOS at 22 tokens, 1.61x vs M1 Ultra). The pass count rises from 91 to 92, the fail count drops from 5 to 4, qwen2.5-vl-3b-4bit leaves both the Known Issues table and the mlx-lm unsupported-architecture list, and it gains a real mlx-lm decode comparison row (156.83 vs 98.53 tok/s, 159%).
@inureyes inureyes added this to the 0.1 milestone Jun 21, 2026
@inureyes inureyes self-assigned this Aug 31, 2026
@inureyes inureyes added the status:done Completed label Sep 6, 2026
be-free-snowboy pushed a commit to freestylejs/snowbrain-mlxcel that referenced this pull request Oct 27, 2026
VLM single-row generation paths (CLI and `bench` invocations that do not carry a `SequenceId`) were leaking fallback cache state between runs. `CxxGenerator::generate_streaming{,_with_embeddings}` and `generate_with_stats{,_and_embeddings}` only called `self.reset()`, which clears the generator-owned `KVCache` vector but never touches the model-owned fallback slot inside `ModelOwnedSequenceState`. A second VLM prefill then reused offsets from the previous run against a freshly built 4D attention mask, producing wrong outputs or panicking when the cached position fell outside the new mask.

Introduce a new `LanguageModel::reset_runtime_state()` trait hook with a default no-op body (correct for models whose entire state lives in the caller's `KVCache` slice) and call it from `CxxGenerator::reset_with_model`. Switch every single-row generation path off the bare `reset()` and onto `reset_with_model(model)` so the generator-owned caches and the model-owned fallback caches are cleared in lockstep. Per-sequence scheduler entries are intentionally untouched — only the legacy fallback slot used by `SequenceId`-less paths is reset.

Per-model implementations: Gemma 3, Gemma 4, and Llama 4 forward the hook to their existing `reset_caches()` routines. Qwen 3.5 Next instead calls `sequence_state.replace_internal(make_internal_caches())` directly so the VLM-populated MRoPE fallback slot is preserved — VLM callers write that slot during embedding preparation immediately before generation begins. `Gemma4VLModel` and `Qwen35VLModel` delegate the hook to their text backbones, and both `VisionLanguageModel` and `LoadedModel` propagate it through their existing `delegate_language_model!` machinery so every wrapper layer participates.

Add `tests/vlm_concurrency.rs` — an ignored, weight-gated smoke test that drives two concurrent image chat requests against a real `mlxcel-server` Gemma 3n VLM instance and asserts both responses come back without cross-contamination. Add a `reset_with_model_invokes_runtime_state_hook` unit test in `mlxcel-core` that uses a tracking model to assert `reset_with_model` actually invokes the new hook, so future single-row paths cannot silently regress past it.
be-free-snowboy pushed a commit to freestylejs/snowbrain-mlxcel that referenced this pull request Oct 27, 2026
Bump version to 0.0.29. Highlights: model-owned VLM fallback state reset (lablup#34), Gemma 3n VLM padded prefill alignment plus the new mlxcel-bench-decode same-process benchmark harness (lablup#36), and benchmark report reorganization with refreshed Apple Silicon and M5 Max VLM tables (lablup#32, lablup#33).
be-free-snowboy pushed a commit to freestylejs/snowbrain-mlxcel that referenced this pull request Oct 27, 2026
`qwen2.5-vl-3b-4bit` was marked FAIL:warmup in the M5 Max benchmark sweep. The failure surfaced in the same-process bench harness: a warmup pass left stale generator-owned KV state, and the subsequent measured pass broadcast a 4-D VLM attention mask against those stale keys and aborted. PR lablup#34 (`fix: reset model-owned VLM fallback state`) fixed this transitively by rebuilding generator-owned KV caches before each single-row run in `CxxGenerator::reset_with_model()`; `qwen2_5_vl` carries no model-owned fallback state, so no additional production code change is needed here.

This commit adds the regression test `qwen2_5_vl_single_row_bench_prefill_succeeds_with_m5_warmup` in `tests/vlm_concurrency.rs`, which drives the exact FAIL:warmup repro path — an image VLM run with `--warmup-tokens 20` so the harness performs both a warmup pass and a measured pass in the same process. It mirrors the existing `gemma3n_vlm_single_row_bench_prefill_accepts_m5_tile_padding` guard and is `#[ignore]`d behind local Qwen2.5-VL weights and the `mlxcel-bench-decode` binary.

The M5 Max benchmark doc (`docs/benchmark_results/model_tests_m5max.md`) is updated to reflect the now-passing run: text prefill 1529.43 tok/s / decode 165.11 tok/s (EOS at 39 tokens) and VLM prefill 1696.53 tok/s / decode 156.83 tok/s (EOS at 22 tokens, 1.61x vs M1 Ultra). The pass count rises from 91 to 92, the fail count drops from 5 to 4, qwen2.5-vl-3b-4bit leaves both the Known Issues table and the mlx-lm unsupported-architecture list, and it gains a real mlx-lm decode comparison row (156.83 vs 98.53 tok/s, 159%).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core mlxcel-core: MLX FFI, primitives, KV cache, layers area:models Model architectures, weights, loading, metadata priority:high High priority status:done Completed type:bug Bug fixes, error corrections, or issue resolutions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant