Skip to content

Fused MRoPE - #8

Merged
wplf merged 5 commits into
wplf:jinliangl/qwen35-vl-central-devfrom
BestJuly:lit/fused_mrope
May 30, 2026
Merged

Fused MRoPE#8
wplf merged 5 commits into
wplf:jinliangl/qwen35-vl-central-devfrom
BestJuly:lit/fused_mrope

Conversation

@BestJuly

@BestJuly BestJuly commented May 29, 2026

Copy link
Copy Markdown

What does this PR do ?

Integrate Triton-based fused multimodal RoPE (mRoPE) kernels (standard BSHD and THD-packed layouts) and wire them into the GPT decoder and the Qwen3.5-VL vision encoder, with full autograd support and a safe unfused fallback whenever fusion is unavailable.

Implementation

  • New megatron/core/fusions/fused_mrope.py: Triton forward/backward kernels for BSHD and THD layouts, a raw→materialized conversion helper (mrope_freqs_to_rotary_emb), and capability/availability gates (get_fused_mrope_unavailable_reason, get_fused_mrope_thd_unavailable_reason).
  • gpt_model._preprocess requests raw per-axis mRoPE frequencies when fusion is available; rope_utils.apply_rotary_pos_emb dispatches fused vs. unfused and centralizes one-time fallback warnings.
  • Qwen3.5-VL vision RoPE is exposed as sectioned raw mRoPE (zero temporal section) so the vision tower reuses the fused THD kernel; optional fp32 compute.
  • apply_rope_fusion validation relaxed to accept Triton fused mRoPE as a backend alongside Transformer Engine; CP-aware THD handling, incl. the odd-local-seqlen fix.

A detailed auto-generated breakdown is in Summary by Sourcery below.

Validation & numerical verification

Operator-level backward — verified correct three independent ways (all errors at fp32 epsilon ~1e-7; covers BSHD + THD, interleaved + non-interleaved, including the real head_dim=256 / rotary_dim=64 / mrope_section=[11,11,10] shape):

  • vs an independent from-scratch PyTorch mRoPE reference: max grad error 2.4e-7
  • torch.autograd.gradcheck: PASSED
  • adjoint identity <R·v, g> == <v, grad> (RoPE is linear): ~1e-7

Coverage: added forward/backward parity tests at the real Qwen3.5-VL deployment shape (head_dim=256, rotary_dim=64, 75% pass-through, [11,11,10]), plus non-interleaved and full-rotary variants, for both BSHD and THD — the previous parametrized tests only covered head_dim=16/20 with rotary_dim=16 (~80% rotated). Unit suite: 65 passed on GB200.

Robustness: the fused THD launch path now enforces per-sequence CP divisibility in get_fused_mrope_thd_unavailable_reason (not just total length). Variable-length packing where the total is CP-divisible but an individual sub-sequence is not now cleanly falls back to the unfused path instead of silently miscomputing global_start // cp_size token indices.

End-to-end (Qwen3.5-VL 397B proxy, GB200): fused vs. unfused are numerically equivalent in the forward and backward of every isolated component and of the full single-GPU multimodal model (grad-norm ratio 1.000, loss matches, ~+3–4% throughput).

⚠️ Note on large expert-parallel runs. With EP=8 a grad-norm difference between the fused and unfused paths is observed. It is a benign distributed numerical effect, not a kernel defect: the legitimate bf16 rounding difference between the two kernels is amplified by the order-dependent (non-associative) cross-rank reductions in the MoE expert-parallel all-to-all, together with clip_grad. It scales with EP degree (grad-norm ratio ≈ 1.00 dense → 0.35 at EP4 → 0.18 at EP8) and disappears entirely without MoE/EP. The fused kernel is numerically correct; enabling fusion simply puts the run on a different (equally valid) bf16 trajectory, so fused and unfused should not be compared bitwise at large EP.

Issue tracking

Linked issue:

Contribution process

Pre-checks

  • I have added relevant unit tests
  • I have added relevant functional tests
  • I have added proper typing to my code Typing guidelines
  • I have added relevant documentation
  • I have run the autoformatter.sh on my PR

Merge

All PRs start as draft; mark Ready for Review once merge-conflicts are resolved and CI is passing. For PRs that change megatron/core, expert + final reviewers are assigned automatically via .github/CODEOWNERS. Any member of mcore-engineers can merge.

Summary by Sourcery

Integrate Triton-based fused multimodal RoPE (mRoPE) kernels and wire them into GPT and Qwen3.5-VL vision models while preserving unfused fallbacks and improving RoPE fusion configurability.

New Features:

  • Add Triton fused multimodal RoPE kernels for standard and THD-packed layouts with public helpers and autograd support.
  • Enable GPT mRoPE to request and consume raw per-axis frequency tensors for fused application when available.
  • Expose Qwen3.5-VL vision RoPE as sectioned raw mRoPE frequencies and dispatch to fused THD mRoPE, including optional fp32 compute.
  • Extend the Qwen3.5-VL training script with new knobs for MTP layers, linear attention frequency, dataset/tokenizer selection, RoPE fusion toggling, and profiling options including NVTX ranges.

Bug Fixes:

  • Correct THD context-parallel token indexing to handle odd local sequence lengths consistently across fused kernels.
  • Enforce per-sequence context-parallel divisibility on the fused THD launch path so variable-length packs fall back safely instead of miscomputing CP indices.

Enhancements:

  • Generalize RoPE utilities to detect and convert raw mRoPE frequency tensors, add context-parallel aware packing helpers, and centralize one-time fusion fallback warnings.
  • Relax apply_rope_fusion validation to also accept Triton fused mRoPE as a backend alongside Transformer Engine and handle mRoPE-specific capability checks.
  • Ensure packed-sequence multimodal RoPE preserves full sequence frequencies under context parallelism and skips CP slicing when appropriate.
  • Propagate language model RoPE fusion settings to the Qwen3.5-VL vision encoder configuration and update attention modules to honor mRoPE interleaving options.

Tests:

  • Add extensive unit tests for fused mRoPE and THD kernels, covering forward/backward accuracy, option-driven fallbacks, context-parallel behavior, and interaction with GPT mRoPE dispatch.
  • Add multimodal tests validating Qwen3.5-VL vision RoPE raw-frequency generation, fused THD dispatch, and numerical parity between fused and unfused paths on CUDA.
  • Add forward/backward parity at the real Qwen3.5-VL deployment shape (head_dim=256, rotary_dim=64) and a regression test for per-sequence CP divisibility.

BestJuly added 3 commits May 29, 2026 03:29
Squash the MRoPE fusion changes from lit/qwen35-mrope-fusion onto jinliangl/qwen35-vl-hybridep-deploy for benchmark deployment.

Source commits: 71dc40c, d2bd427, 0885b1d, 39187d8, b529407, a604225ff, ab075bee0.
@sourcery-ai

sourcery-ai Bot commented May 29, 2026

Copy link
Copy Markdown

Reviewer's Guide

Integrates a new Triton-based fused multimodal RoPE (mRoPE) implementation for both standard and THD-packed layouts, wires it into GPT and Qwen3.5-VL vision paths with configuration/dispatch logic and detailed fallbacks, and extends scripts/tests to cover fusion behavior, NVTX profiling, and multimodal configs.

Sequence diagram for fused mRoPE dispatch in apply_rotary_pos_emb

sequenceDiagram
    participant GPTModel as GPTModel_preprocess
    participant MRoPE as MultimodalRotaryEmbedding_forward
    participant Rope as apply_rotary_pos_emb
    participant Triton as fused_apply_mrope
    participant TE as fused_apply_rotary_pos_emb
    participant Unfused as _apply_rotary_pos_emb_bshd

    GPTModel->>MRoPE: rotary_pos_emb(position_ids, mrope_section,\n cp_group, return_raw_freqs, packed_seq)
    MRoPE-->>GPTModel: freqs_or_emb
    GPTModel->>Rope: apply_rotary_pos_emb(t, freqs, config,\n cu_seqlens=None,...)

    alt apply_rope_fusion and raw mRoPE freqs
        Rope->>Triton: fused_apply_mrope(t, freqs, mrope_section,\n interleaved_mrope, rotary_interleaved=False)
        Triton-->>Rope: rotated_t
        Rope-->>GPTModel: rotated_t
    else TE fused RoPE available
        Rope->>TE: fused_apply_rotary_pos_emb(t, freqs,\n interleaved=config.rotary_interleaved)
        TE-->>Rope: rotated_t
        Rope-->>GPTModel: rotated_t
    else
        Rope->>Unfused: _apply_rotary_pos_emb_bshd(t, freqs,...)
        Unfused-->>Rope: rotated_t
        Rope-->>GPTModel: rotated_t
    end
Loading

File-Level Changes

Change Details Files
Add Triton fused mRoPE kernels and public API, including availability checks and conversion from raw mRoPE freqs to legacy rotary embeddings.
  • Introduce megatron.core.fusions.fused_mrope with Triton kernels for BSHD and THD layouts and corresponding autograd Functions.
  • Provide helper APIs (is_fused_mrope_available, can_launch_fused_mrope[_thd], get_fused_mrope*_unavailable_reason) to gate fusion based on device, dtype, layout, and capability.
  • Implement mrope_freqs_to_rotary_emb to convert raw 3-axis mRoPE freqs into the existing rotary_pos_emb layout, supporting both sectioned and interleaved Qwen-style formats.
megatron/core/fusions/fused_mrope.py
Extend RoPE utilities to detect and handle raw mRoPE frequency tensors, dispatch to Triton fused kernels when allowed, and centralize one-time warning behavior for fusion fallbacks.
  • Import fused mRoPE APIs into rope_utils and expose them in all for external use.
  • Add helpers to recognize raw 3-axis mRoPE freqs in both BSHD and THD layouts, validate THD shapes/CP constraints, and convert raw freqs to rotary embeddings.
  • Implement THD-specific helpers to compute CP slices of raw mRoPE freqs and pack them to match THD token ordering for unfused fallbacks.
  • Introduce _warn_rope_fusion_fallback_once and a categorized warning-key generator to emit per-cause, single-shot warnings when fusion is unavailable or unsupported for given options.
  • Update apply_rotary_pos_emb to (1) prefer Triton fused mRoPE when seeing raw freqs and compatible options, (2) fall back with precise warnings and conversion to materialized embeddings, and (3) handle both BSHD and THD/packed-sequence paths with TE vs Triton routing.
megatron/core/models/common/embeddings/rope_utils.py
Update MultimodalRoPE and GPT model preprocessing to generate and consume raw mRoPE freqs for fusion while preserving behavior for materialized embeddings and inference modes.
  • Extend MultimodalRotaryEmbedding.forward with return_raw_freqs and packed_seq flags to optionally return raw per-axis freqs and to control CP slicing behavior for packed sequences.
  • Adjust the rotary embedding construction logic to use reshape instead of view and to avoid CP slicing when packed_seq=True.
  • In GPTModel.init, probe Triton fused mRoPE availability when apply_rope_fusion and mRoPE are enabled and rotary_interleaved is False, caching the result on the model.
  • In GPTModel._preprocess, decide per-call whether to request raw mRoPE freqs from the embedding (based on fusion availability, fused_single_qkv_rope, packed_seq, and inference mode) and pass packed_seq through to the embedding so training packed paths use global raw freqs correctly.
megatron/core/models/common/embeddings/rotary_pos_embedding.py
megatron/core/models/gpt/gpt_model.py
Relax and extend TransformerConfig and training argument validation to support mRoPE fusion as an additional RoPE backend.
  • In TransformerConfig.post_init, detect fused mRoPE availability (for non-rotary_interleaved mRoPE) and incorporate it into the apply_rope_fusion capability check alongside TE fused RoPE.
  • Adjust rotary_interleaved validation so TE-version requirements only apply when fused mRoPE is not available.
  • Update the error message when apply_rope_fusion is not available to mention both TE and Triton fused mRoPE as installation options.
  • Allow apply_rope_fusion to stay enabled for position_embedding_type='mrope' in validate_args while disabling it for other non-RoPE types.
megatron/core/transformer/transformer_config.py
megatron/training/arguments.py
Wire fused mRoPE into Qwen3.5-VL vision encoder and its FP32 RoPE wrapper, adding raw mRoPE construction and THD fused dispatch plus profiling aids.
  • Modify Qwen35VLVisionEncoder._compute_rotary_pos_emb to return raw sectioned 3-axis freqs when mrope_section is configured, validating section layout and constructing [3,1,total_patches,head_dim/2].
  • Update vision forward path to pass raw freqs directly as rotary_pos_emb when mRoPE is enabled, otherwise keep legacy materialized embedding behavior.
  • Change the Qwen vision FP32 RoPE wrapper to first try fused_apply_mrope_thd (with availability check and fp32_compute=True) when conditions allow, otherwise delegate to generic apply_rotary_pos_emb and let standard fusion/unfused paths handle it.
  • Wrap the no-CP FP32 RoPE helper in an NVTX range for easier profiling of vision RoPE when profiling is enabled.
examples/multimodal_dev/models/qwen35_vl/vision_encoder.py
examples/multimodal_dev/models/qwen35_vl/specs.py
Set up Qwen3.5-VL vision TransformerConfig for 2D mRoPE and keep vision language/rope-fusion configs aligned.
  • In Qwen35-VL vision configuration, assert kv_channels is divisible by 4, derive per-axis RoPE dimension, and configure mrope_section=[0, axis_dim, axis_dim] with mrope_interleaved=False and rotary_interleaved=False.
  • Ensure the vision config inherits apply_rope_fusion from the language config in pretrain_multimodal model_provider so both branches use consistent fusion settings.
examples/multimodal_dev/models/qwen35_vl/configuration.py
examples/multimodal_dev/pretrain_multimodal.py
Improve fused MLA/YaRN THD token index mapping to align with the new odd-length CP segmentation scheme.
  • Update _get_thd_token_idx to compute first and second CP segments using the same odd/even split logic (first_cp_seg=(len+1)//2, second_cp_seg=len//2) as the new THD RoPE mapping, and adjust the backward-half indexing formula accordingly.
megatron/core/fusions/fused_mla_yarn_rope_apply.py
Enhance the Qwen3.5-VL training script with mRoPE- and profiling-related controls and better configurability for experiments.
  • Add environment-variable-driven toggles for RoPE fusion (--no-rope-fusion), NVTX ranges, checkpoint saving, MTP layers, linear attention frequency, dataset provider, tokenizer type/null tokenizer, image seq length, HF processor path, launcher Python, and torchrun invocation style.
  • Inject these options into the constructed TRAINING_ARGS, PROFILE_ARGS, EVAL_AND_LOGGING_ARGS, TOKENIZER_ARGS, MULTIMODAL_ARGS, and GPT_MODEL_ARGS arrays as appropriate, including appending --nvtx-ranges when requested.
  • Extend experiment name and startup logging to reflect new toggles (RoPE fusion on/off, dataset/tokenizer choice, checkpoint saving, MTP/linear-attention settings, launcher details).
examples/multimodal_dev/scripts/run_qwen35_vl.sh
Introduce comprehensive unit tests for fused mRoPE kernels, dispatch logic, fallbacks, and Qwen3.5-VL vision RoPE behavior.
  • Add tests for fused_mrope BSHD and THD kernels (forward/backward parity, CP behavior, fp32 compute option, odd/variable sequence lengths, and public APIs like get_fused_mrope*_unavailable_reason).
  • Exercise apply_rotary_pos_emb dispatch paths for raw vs materialized freqs, TE vs Triton, mscale/inverse/MLA/rotary_interleaved fallbacks, CPU/dtype/stride/capability gating, and single-shot warning key behavior.
  • Test GPT mRoPE preprocessing decisions around raw freqs vs materialized embeddings, fused_single_qkv_rope, packed_seq, and inference contexts (dynamic/static).
  • Add multimodal/vision tests ensuring Qwen3.5-VL vision config sets 2D mRoPE correctly, raw freq construction matches legacy materialized RoPE, and the FP32 wrapper routes THD packed raw freqs to fused mRoPE with the right parameters, plus an end-to-end CUDA check comparing fused vs unfused paths.
tests/unit_tests/fusions/test_fused_mrope.py
examples/multimodal_dev/tests/test_vision_rope_fusion.py
Minor cleanup and helper adjustments to support the new fusion paths.
  • Simplify an import in Qwen35-VL vision_encoder to a single-line import for get_qwen35_vl_vision_spec.
  • Clarify the role of the _NoCPGroup dummy group in the multimodal base module now that packed-seq MRoPE callsites can skip CP slicing via packed_seq=True.
  • Adjust attention._build_per_layer_rotary_pos_emb to pass config.mrope_interleaved into the MultimodalRotaryEmbedding constructor so fused and unfused layouts stay consistent across layers.
examples/multimodal_dev/models/qwen35_vl/vision_encoder.py
examples/multimodal_dev/models/base.py
megatron/core/transformer/attention.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • The THD context-parallel index arithmetic for zigzag splits is now implemented in multiple places (e.g., _get_thd_freqs_on_this_cp_rank, _get_thd_raw_mrope_freqs_on_this_cp_rank, _get_thd_cp_splits, and _get_thd_token_idx in fused_mla_yarn_rope_apply); consider factoring this into a single shared helper to avoid drift and make future changes less error-prone.
  • The logic that toggles between raw mRoPE freqs and materialized rotary embeddings is spread across MultimodalRotaryEmbedding.forward, apply_rotary_pos_emb, and GPTModel._preprocess; it may be worth centralizing this policy (e.g., a helper that decides when to return raw vs materialized based on config/inference mode) to reduce duplication and keep behavior consistent across call sites.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The THD context-parallel index arithmetic for zigzag splits is now implemented in multiple places (e.g., `_get_thd_freqs_on_this_cp_rank`, `_get_thd_raw_mrope_freqs_on_this_cp_rank`, `_get_thd_cp_splits`, and `_get_thd_token_idx` in `fused_mla_yarn_rope_apply`); consider factoring this into a single shared helper to avoid drift and make future changes less error-prone.
- The logic that toggles between raw mRoPE freqs and materialized rotary embeddings is spread across `MultimodalRotaryEmbedding.forward`, `apply_rotary_pos_emb`, and `GPTModel._preprocess`; it may be worth centralizing this policy (e.g., a helper that decides when to return raw vs materialized based on config/inference mode) to reduce duplication and keep behavior consistent across call sites.

## Individual Comments

### Comment 1
<location path="megatron/core/models/common/embeddings/rope_utils.py" line_range="505-507" />
<code_context>
     if cp_group is None:
         cp_group = parallel_state.get_context_parallel_group()

+    is_raw_mrope_freqs = (
+        _is_raw_mrope_freqs(t, freqs, config)
+        if cu_seqlens is None
+        else _is_raw_mrope_freqs_thd(t, freqs, cu_seqlens, config, cp_group.size())
+    )
</code_context>
<issue_to_address>
**issue (bug_risk):** Guard `cp_group` before calling `.size()` in THD mRoPE detection to avoid `NoneType` errors.

In the THD branch, `_is_raw_mrope_freqs_thd` is called with `cp_group.size()`, but `cp_group` comes from `parallel_state.get_context_parallel_group()`, which can be `None` in non-CP setups. That will raise an AttributeError before the later `_apply_rotary_pos_emb_thd` check that handles `cp_group` explicitly.

Consider either failing fast with a clear assertion/error when `cp_group` is `None` in the THD path, or treating `cp_group is None` as `cp_size == 1` and passing `cp_size=1` into `_is_raw_mrope_freqs_thd`, so the behavior and error messaging remain intentional rather than a `NoneType` attribute error.
</issue_to_address>

### Comment 2
<location path="tests/unit_tests/fusions/test_fused_mrope.py" line_range="195-196" />
<code_context>
+    assert len(set(actual)) == len(actual)
+
+
+@pytest.mark.parametrize("use_packed_seq", [False, True])
+def test_gpt_mrope_eval_requests_raw_freqs_when_fusion_available(use_packed_seq):
+    captured_kwargs = {}
+
</code_context>
<issue_to_address>
**suggestion (testing):** Add a regression test that covers the fallback path when fused mRoPE is not available in GPT `_preprocess`.

To increase coverage, please also exercise the branch where `_fused_mrope_available` (or `apply_rope_fusion`) is `False` and confirm `_preprocess` continues to use materialized RoPE embeddings instead of raw freqs. For example, parametrize this test (or add a sibling test) to run with `_fused_mrope_available=False` and assert that `return_raw_freqs` stays `False` and `rotary_pos_emb` comes from the materialized embedding path.

Suggested implementation:

```python
+@pytest.mark.parametrize("use_packed_seq", [False, True])
+@pytest.mark.parametrize("fused_available", [False, True])
+def test_gpt_mrope_eval_requests_raw_freqs_when_fusion_available(
+    use_packed_seq, fused_available, monkeypatch
+):
+    """
+    When fused mRoPE is available, GPT._preprocess should request raw freqs.
+    When it is not available, GPT._preprocess should fall back to materialized RoPE
+    embeddings and keep return_raw_freqs=False.
+    """
+    captured_kwargs = {}
+
+    # Patch the fused mRoPE availability / fusion entry point that _preprocess consults.
+    # We capture the arguments to inspect return_raw_freqs and rotary_pos_emb.
+    def _capturing_fused_apply_mrope(*args, **kwargs):
+        captured_kwargs.update(kwargs)
+        # We do not care about the numerical result here; let the original function run
+        # when fusion is "available", and raise when it's disabled so we know that path
+        # is not mistakenly taken.
+        if not fused_available:
+            raise AssertionError("fused_apply_mrope should not be called when fusion is disabled")
+        return _orig_fused_apply_mrope(*args, **kwargs)
+
+    _orig_fused_apply_mrope = fused_apply_mrope
+    monkeypatch.setattr(
+        rope_utils,
+        "apply_rope_fusion",
+        lambda: fused_available,
+        raising=False,
+    )
+    monkeypatch.setattr(
+        "megatron.core.fusions.fused_mrope.fused_apply_mrope",
+        _capturing_fused_apply_mrope,
+    )
+
+    # Build a tiny GPT model configured to use mRoPE in eval mode.
+    # The helper should return a model where forward() goes through _preprocess.
+    gpt = build_gpt_for_test(use_packed_seq=use_packed_seq, use_mrope=True)
+    gpt.eval()
+
+    # Run one forward pass to trigger _preprocess and (potential) fusion.
+    batch_size, seq_len = 2, 8
+    input_ids = torch.randint(0, gpt.config.vocab_size, (batch_size, seq_len), device="cuda")
+    with torch.no_grad():
+        _ = gpt(input_ids)
+
+    if fused_available:
+        # Fusion path: raw freqs should be requested.
+        assert captured_kwargs, "Expected fused_apply_mrope to be called when fusion is available"
+        assert captured_kwargs.get("return_raw_freqs", False) is True
+    else:
+        # Fallback path: fused_apply_mrope must not be called, and _preprocess should
+        # have used materialized rotary_pos_emb instead of raw freqs.
+        assert captured_kwargs == {}, "fused_apply_mrope should not be used when fusion is disabled"
+        # Inspect the rope state attached to the model to ensure materialized embeddings were used.
+        rope_state = getattr(gpt, "_rope_state", None)
+        assert rope_state is not None, "Expected GPT to store RoPE state for materialized embeddings"
+        assert rope_state.return_raw_freqs is False
+        assert rope_state.rotary_pos_emb is not None
+        assert isinstance(rope_state.rotary_pos_emb, torch.Tensor)
+

```

`.

```xml
<file_operations>
<file_operation operation="edit" file_path="tests/unit_tests/fusions/test_fused_mrope.py">
<<<<<<< SEARCH
+@pytest.mark.parametrize("use_packed_seq", [False, True])
+def test_gpt_mrope_eval_requests_raw_freqs_when_fusion_available(use_packed_seq):
+    captured_kwargs = {}
+
=======
+@pytest.mark.parametrize("use_packed_seq", [False, True])
+@pytest.mark.parametrize("fused_available", [False, True])
+def test_gpt_mrope_eval_requests_raw_freqs_when_fusion_available(
+    use_packed_seq, fused_available, monkeypatch
+):
+    """
+    When fused mRoPE is available, GPT._preprocess should request raw freqs.
+    When it is not available, GPT._preprocess should fall back to materialized RoPE
+    embeddings and keep return_raw_freqs=False.
+    """
+    captured_kwargs = {}
+
+    # Patch the fused mRoPE availability / fusion entry point that _preprocess consults.
+    # We capture the arguments to inspect return_raw_freqs and rotary_pos_emb.
+    def _capturing_fused_apply_mrope(*args, **kwargs):
+        captured_kwargs.update(kwargs)
+        # We do not care about the numerical result here; let the original function run
+        # when fusion is "available", and raise when it's disabled so we know that path
+        # is not mistakenly taken.
+        if not fused_available:
+            raise AssertionError("fused_apply_mrope should not be called when fusion is disabled")
+        return _orig_fused_apply_mrope(*args, **kwargs)
+
+    _orig_fused_apply_mrope = fused_apply_mrope
+    monkeypatch.setattr(
+        rope_utils,
+        "apply_rope_fusion",
+        lambda: fused_available,
+        raising=False,
+    )
+    monkeypatch.setattr(
+        "megatron.core.fusions.fused_mrope.fused_apply_mrope",
+        _capturing_fused_apply_mrope,
+    )
+
+    # Build a tiny GPT model configured to use mRoPE in eval mode.
+    # The helper should return a model where forward() goes through _preprocess.
+    gpt = build_gpt_for_test(use_packed_seq=use_packed_seq, use_mrope=True)
+    gpt.eval()
+
+    # Run one forward pass to trigger _preprocess and (potential) fusion.
+    batch_size, seq_len = 2, 8
+    input_ids = torch.randint(0, gpt.config.vocab_size, (batch_size, seq_len), device="cuda")
+    with torch.no_grad():
+        _ = gpt(input_ids)
+
+    if fused_available:
+        # Fusion path: raw freqs should be requested.
+        assert captured_kwargs, "Expected fused_apply_mrope to be called when fusion is available"
+        assert captured_kwargs.get("return_raw_freqs", False) is True
+    else:
+        # Fallback path: fused_apply_mrope must not be called, and _preprocess should
+        # have used materialized rotary_pos_emb instead of raw freqs.
+        assert captured_kwargs == {}, "fused_apply_mrope should not be used when fusion is disabled"
+        # Inspect the rope state attached to the model to ensure materialized embeddings were used.
+        rope_state = getattr(gpt, "_rope_state", None)
+        assert rope_state is not None, "Expected GPT to store RoPE state for materialized embeddings"
+        assert rope_state.return_raw_freqs is False
+        assert rope_state.rotary_pos_emb is not None
+        assert isinstance(rope_state.rotary_pos_emb, torch.Tensor)
+
>>>>>>> REPLACE
</file_operation>
</file_operations>

<additional_changes>
1. Ensure there is an import for `build_gpt_for_test` or adjust the construction of the GPT model to match your existing test helpers. For example, if you already use a `build_model` or `get_gpt_model` helper elsewhere, use that instead of `build_gpt_for_test`.
2. The test assumes that:
   - `rope_utils.apply_rope_fusion()` (or a similarly named function) is what `_preprocess` checks to determine fusion availability. If the real name differs, update the `monkeypatch.setattr(rope_utils, "apply_rope_fusion", ...)` call accordingly.
   - GPT attaches RoPE-related information to `gpt._rope_state` with attributes `return_raw_freqs` and `rotary_pos_emb`. If your implementation exposes these under a different name or location, change the `rope_state` lookup and assertions to match.
3. If `_preprocess` does not call `fused_apply_mrope` directly but goes through another wrapper, adjust the `monkeypatch.setattr("megatron.core.fusions.fused_mrope.fused_apply_mrope", ...)` target string to hook the actually-called symbol.
4. If your tests do not run on CUDA, change `device="cuda"` to `"cpu"` or use `gpt.device` / `next(gpt.parameters()).device` so the tensor is on the correct device.
</issue_to_address>

### Comment 3
<location path="examples/multimodal_dev/tests/test_vision_rope_fusion.py" line_range="71-80" />
<code_context>
+def test_vision_fp32_wrapper_dispatches_raw_freqs_to_fused_mrope_thd(monkeypatch):
</code_context>
<issue_to_address>
**suggestion (testing):** Complement this positive-path dispatch test with a fallback-path test where fused mRoPE THD is unavailable.

To complete coverage of this wrapper, please also add a test where `get_fused_mrope_thd_unavailable_reason` returns a non-`None` string and verify that `_apply_rope_fp32_no_cp` falls back to the unfused `apply_rotary_pos_emb` path (e.g., by asserting `fake_fused_apply_mrope_thd` is not called and the rope-utils path runs instead). This will help catch regressions in the fallback branch for vision THD RoPE.

Suggested implementation:

```python
    torch.testing.assert_close(converted, expected)


def test_vision_fp32_wrapper_dispatches_fallback_when_fused_mrope_thd_unavailable(monkeypatch):
    # We want to verify that when fused mRoPE THD is unavailable, the wrapper
    # falls back to the unfused `apply_rotary_pos_emb` path.

    # Local imports to avoid affecting other tests; adjust module paths as needed.
    import torch

    # The module under test and its dependency names will need to match the
    # actual implementation; these strings may need adjustment.
    module_path = "examples.multimodal_dev.vision_rope_fusion"

    calls = {
        "fused_mrope": 0,
        "unfused_rope": 0,
    }

    def fake_get_fused_mrope_thd_unavailable_reason() -> str:
        # Non-None return value should trigger the fallback path.
        return "test: fused mRoPE THD unavailable"

    def fake_fused_apply_mrope_thd(
        t,
        cu_seqlens,
        freqs,
        mrope_section,
        interleaved_mrope=False,
        rotary_interleaved=False,
        cp_size=1,
    ):
        # If this gets called in this test, the wrapper did not take the fallback.
        calls["fused_mrope"] += 1
        return t

    def fake_apply_rotary_pos_emb(x, freqs, *args, **kwargs):
        # Track that the unfused RoPE path ran.
        calls["unfused_rope"] += 1
        return x

    # Monkeypatch the unavailability probe to force the fallback path.
    monkeypatch.setattr(
        f"{module_path}.get_fused_mrope_thd_unavailable_reason",
        lambda: fake_get_fused_mrope_thd_unavailable_reason(),
        raising=True,
    )

    # Monkeypatch both the fused mRoPE implementation and the unfused RoPE util.
    monkeypatch.setattr(
        f"{module_path}.apply_fused_mrope_thd",
        fake_fused_apply_mrope_thd,
        raising=True,
    )
    monkeypatch.setattr(
        f"{module_path}.apply_rotary_pos_emb",
        fake_apply_rotary_pos_emb,
        raising=True,
    )

    # Import the wrapper under test after monkeypatching so it picks up patched symbols.
    from examples.multimodal_dev.vision_rope_fusion import _apply_rope_fp32_no_cp

    # Minimal tensor inputs; precise shapes are not important for this behavioral test.
    t = torch.randn(2, 4, 8, dtype=torch.float32)
    cu_seqlens = torch.tensor([0, 2], dtype=torch.int32)
    freqs = torch.randn(1, 1, 4, 4, dtype=torch.float32)
    mrope_section = [0, 4, 4]

    # Call the wrapper; with fused mRoPE marked unavailable, it should take the fallback.
    _apply_rope_fp32_no_cp(
        t=t,
        cu_seqlens=cu_seqlens,
        freqs=freqs,
        mrope_section=mrope_section,
        interleaved_mrope=False,
        rotary_interleaved=False,
    )

    # Assert that the fused path was not used and the unfused RoPE path was taken.
    assert calls["fused_mrope"] == 0
    assert calls["unfused_rope"] == 1

```

1. Update `module_path = "examples.multimodal_dev.vision_rope_fusion"` and the two monkeypatch targets (`apply_fused_mrope_thd`, `apply_rotary_pos_emb`) to match the actual module and function names used by `_apply_rope_fp32_no_cp` in your codebase. The key is that:
   - `get_fused_mrope_thd_unavailable_reason` is patched to return a non-`None` string.
   - The fused mRoPE THD implementation used by `_apply_rope_fp32_no_cp` is patched as `fake_fused_apply_mrope_thd`.
   - The unfused RoPE helper called by `_apply_rope_fp32_no_cp` is patched as `fake_apply_rotary_pos_emb`.
2. If `_apply_rope_fp32_no_cp` requires additional parameters in your implementation (e.g., `dtype`, `device`, or vision-specific flags), pass appropriate dummy values in the test call so it exercises the exact fallback branch you want to cover.
3. If the helper functions are imported into the module under different local names (e.g., `from ... import apply_rotary_pos_emb as apply_vision_rope`), adjust the monkeypatch targets to those names so the patched functions are actually used.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +505 to +507
is_raw_mrope_freqs = (
_is_raw_mrope_freqs(t, freqs, config)
if cu_seqlens is None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Guard cp_group before calling .size() in THD mRoPE detection to avoid NoneType errors.

In the THD branch, _is_raw_mrope_freqs_thd is called with cp_group.size(), but cp_group comes from parallel_state.get_context_parallel_group(), which can be None in non-CP setups. That will raise an AttributeError before the later _apply_rotary_pos_emb_thd check that handles cp_group explicitly.

Consider either failing fast with a clear assertion/error when cp_group is None in the THD path, or treating cp_group is None as cp_size == 1 and passing cp_size=1 into _is_raw_mrope_freqs_thd, so the behavior and error messaging remain intentional rather than a NoneType attribute error.

Comment on lines +195 to +196
@pytest.mark.parametrize("use_packed_seq", [False, True])
def test_gpt_mrope_eval_requests_raw_freqs_when_fusion_available(use_packed_seq):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add a regression test that covers the fallback path when fused mRoPE is not available in GPT _preprocess.

To increase coverage, please also exercise the branch where _fused_mrope_available (or apply_rope_fusion) is False and confirm _preprocess continues to use materialized RoPE embeddings instead of raw freqs. For example, parametrize this test (or add a sibling test) to run with _fused_mrope_available=False and assert that return_raw_freqs stays False and rotary_pos_emb comes from the materialized embedding path.

Suggested implementation:

+@pytest.mark.parametrize("use_packed_seq", [False, True])
+@pytest.mark.parametrize("fused_available", [False, True])
+def test_gpt_mrope_eval_requests_raw_freqs_when_fusion_available(
+    use_packed_seq, fused_available, monkeypatch
+):
+    """
+    When fused mRoPE is available, GPT._preprocess should request raw freqs.
+    When it is not available, GPT._preprocess should fall back to materialized RoPE
+    embeddings and keep return_raw_freqs=False.
+    """
+    captured_kwargs = {}
+
+    # Patch the fused mRoPE availability / fusion entry point that _preprocess consults.
+    # We capture the arguments to inspect return_raw_freqs and rotary_pos_emb.
+    def _capturing_fused_apply_mrope(*args, **kwargs):
+        captured_kwargs.update(kwargs)
+        # We do not care about the numerical result here; let the original function run
+        # when fusion is "available", and raise when it's disabled so we know that path
+        # is not mistakenly taken.
+        if not fused_available:
+            raise AssertionError("fused_apply_mrope should not be called when fusion is disabled")
+        return _orig_fused_apply_mrope(*args, **kwargs)
+
+    _orig_fused_apply_mrope = fused_apply_mrope
+    monkeypatch.setattr(
+        rope_utils,
+        "apply_rope_fusion",
+        lambda: fused_available,
+        raising=False,
+    )
+    monkeypatch.setattr(
+        "megatron.core.fusions.fused_mrope.fused_apply_mrope",
+        _capturing_fused_apply_mrope,
+    )
+
+    # Build a tiny GPT model configured to use mRoPE in eval mode.
+    # The helper should return a model where forward() goes through _preprocess.
+    gpt = build_gpt_for_test(use_packed_seq=use_packed_seq, use_mrope=True)
+    gpt.eval()
+
+    # Run one forward pass to trigger _preprocess and (potential) fusion.
+    batch_size, seq_len = 2, 8
+    input_ids = torch.randint(0, gpt.config.vocab_size, (batch_size, seq_len), device="cuda")
+    with torch.no_grad():
+        _ = gpt(input_ids)
+
+    if fused_available:
+        # Fusion path: raw freqs should be requested.
+        assert captured_kwargs, "Expected fused_apply_mrope to be called when fusion is available"
+        assert captured_kwargs.get("return_raw_freqs", False) is True
+    else:
+        # Fallback path: fused_apply_mrope must not be called, and _preprocess should
+        # have used materialized rotary_pos_emb instead of raw freqs.
+        assert captured_kwargs == {}, "fused_apply_mrope should not be used when fusion is disabled"
+        # Inspect the rope state attached to the model to ensure materialized embeddings were used.
+        rope_state = getattr(gpt, "_rope_state", None)
+        assert rope_state is not None, "Expected GPT to store RoPE state for materialized embeddings"
+        assert rope_state.return_raw_freqs is False
+        assert rope_state.rotary_pos_emb is not None
+        assert isinstance(rope_state.rotary_pos_emb, torch.Tensor)
+

`.

<file_operations>
<file_operation operation="edit" file_path="tests/unit_tests/fusions/test_fused_mrope.py">
<<<<<<< SEARCH
+@pytest.mark.parametrize("use_packed_seq", [False, True])
+def test_gpt_mrope_eval_requests_raw_freqs_when_fusion_available(use_packed_seq):
+    captured_kwargs = {}
+
=======
+@pytest.mark.parametrize("use_packed_seq", [False, True])
+@pytest.mark.parametrize("fused_available", [False, True])
+def test_gpt_mrope_eval_requests_raw_freqs_when_fusion_available(
+    use_packed_seq, fused_available, monkeypatch
+):
+    """
+    When fused mRoPE is available, GPT._preprocess should request raw freqs.
+    When it is not available, GPT._preprocess should fall back to materialized RoPE
+    embeddings and keep return_raw_freqs=False.
+    """
+    captured_kwargs = {}
+
+    # Patch the fused mRoPE availability / fusion entry point that _preprocess consults.
+    # We capture the arguments to inspect return_raw_freqs and rotary_pos_emb.
+    def _capturing_fused_apply_mrope(*args, **kwargs):
+        captured_kwargs.update(kwargs)
+        # We do not care about the numerical result here; let the original function run
+        # when fusion is "available", and raise when it's disabled so we know that path
+        # is not mistakenly taken.
+        if not fused_available:
+            raise AssertionError("fused_apply_mrope should not be called when fusion is disabled")
+        return _orig_fused_apply_mrope(*args, **kwargs)
+
+    _orig_fused_apply_mrope = fused_apply_mrope
+    monkeypatch.setattr(
+        rope_utils,
+        "apply_rope_fusion",
+        lambda: fused_available,
+        raising=False,
+    )
+    monkeypatch.setattr(
+        "megatron.core.fusions.fused_mrope.fused_apply_mrope",
+        _capturing_fused_apply_mrope,
+    )
+
+    # Build a tiny GPT model configured to use mRoPE in eval mode.
+    # The helper should return a model where forward() goes through _preprocess.
+    gpt = build_gpt_for_test(use_packed_seq=use_packed_seq, use_mrope=True)
+    gpt.eval()
+
+    # Run one forward pass to trigger _preprocess and (potential) fusion.
+    batch_size, seq_len = 2, 8
+    input_ids = torch.randint(0, gpt.config.vocab_size, (batch_size, seq_len), device="cuda")
+    with torch.no_grad():
+        _ = gpt(input_ids)
+
+    if fused_available:
+        # Fusion path: raw freqs should be requested.
+        assert captured_kwargs, "Expected fused_apply_mrope to be called when fusion is available"
+        assert captured_kwargs.get("return_raw_freqs", False) is True
+    else:
+        # Fallback path: fused_apply_mrope must not be called, and _preprocess should
+        # have used materialized rotary_pos_emb instead of raw freqs.
+        assert captured_kwargs == {}, "fused_apply_mrope should not be used when fusion is disabled"
+        # Inspect the rope state attached to the model to ensure materialized embeddings were used.
+        rope_state = getattr(gpt, "_rope_state", None)
+        assert rope_state is not None, "Expected GPT to store RoPE state for materialized embeddings"
+        assert rope_state.return_raw_freqs is False
+        assert rope_state.rotary_pos_emb is not None
+        assert isinstance(rope_state.rotary_pos_emb, torch.Tensor)
+
>>>>>>> REPLACE
</file_operation>
</file_operations>

<additional_changes>
1. Ensure there is an import for `build_gpt_for_test` or adjust the construction of the GPT model to match your existing test helpers. For example, if you already use a `build_model` or `get_gpt_model` helper elsewhere, use that instead of `build_gpt_for_test`.
2. The test assumes that:
   - `rope_utils.apply_rope_fusion()` (or a similarly named function) is what `_preprocess` checks to determine fusion availability. If the real name differs, update the `monkeypatch.setattr(rope_utils, "apply_rope_fusion", ...)` call accordingly.
   - GPT attaches RoPE-related information to `gpt._rope_state` with attributes `return_raw_freqs` and `rotary_pos_emb`. If your implementation exposes these under a different name or location, change the `rope_state` lookup and assertions to match.
3. If `_preprocess` does not call `fused_apply_mrope` directly but goes through another wrapper, adjust the `monkeypatch.setattr("megatron.core.fusions.fused_mrope.fused_apply_mrope", ...)` target string to hook the actually-called symbol.
4. If your tests do not run on CUDA, change `device="cuda"` to `"cpu"` or use `gpt.device` / `next(gpt.parameters()).device` so the tensor is on the correct device.

Comment on lines +71 to +80
def test_vision_fp32_wrapper_dispatches_raw_freqs_to_fused_mrope_thd(monkeypatch):
calls = {}

def fake_fused_apply_mrope_thd(
t,
cu_seqlens,
freqs,
mrope_section,
interleaved_mrope=False,
rotary_interleaved=False,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Complement this positive-path dispatch test with a fallback-path test where fused mRoPE THD is unavailable.

To complete coverage of this wrapper, please also add a test where get_fused_mrope_thd_unavailable_reason returns a non-None string and verify that _apply_rope_fp32_no_cp falls back to the unfused apply_rotary_pos_emb path (e.g., by asserting fake_fused_apply_mrope_thd is not called and the rope-utils path runs instead). This will help catch regressions in the fallback branch for vision THD RoPE.

Suggested implementation:

    torch.testing.assert_close(converted, expected)


def test_vision_fp32_wrapper_dispatches_fallback_when_fused_mrope_thd_unavailable(monkeypatch):
    # We want to verify that when fused mRoPE THD is unavailable, the wrapper
    # falls back to the unfused `apply_rotary_pos_emb` path.

    # Local imports to avoid affecting other tests; adjust module paths as needed.
    import torch

    # The module under test and its dependency names will need to match the
    # actual implementation; these strings may need adjustment.
    module_path = "examples.multimodal_dev.vision_rope_fusion"

    calls = {
        "fused_mrope": 0,
        "unfused_rope": 0,
    }

    def fake_get_fused_mrope_thd_unavailable_reason() -> str:
        # Non-None return value should trigger the fallback path.
        return "test: fused mRoPE THD unavailable"

    def fake_fused_apply_mrope_thd(
        t,
        cu_seqlens,
        freqs,
        mrope_section,
        interleaved_mrope=False,
        rotary_interleaved=False,
        cp_size=1,
    ):
        # If this gets called in this test, the wrapper did not take the fallback.
        calls["fused_mrope"] += 1
        return t

    def fake_apply_rotary_pos_emb(x, freqs, *args, **kwargs):
        # Track that the unfused RoPE path ran.
        calls["unfused_rope"] += 1
        return x

    # Monkeypatch the unavailability probe to force the fallback path.
    monkeypatch.setattr(
        f"{module_path}.get_fused_mrope_thd_unavailable_reason",
        lambda: fake_get_fused_mrope_thd_unavailable_reason(),
        raising=True,
    )

    # Monkeypatch both the fused mRoPE implementation and the unfused RoPE util.
    monkeypatch.setattr(
        f"{module_path}.apply_fused_mrope_thd",
        fake_fused_apply_mrope_thd,
        raising=True,
    )
    monkeypatch.setattr(
        f"{module_path}.apply_rotary_pos_emb",
        fake_apply_rotary_pos_emb,
        raising=True,
    )

    # Import the wrapper under test after monkeypatching so it picks up patched symbols.
    from examples.multimodal_dev.vision_rope_fusion import _apply_rope_fp32_no_cp

    # Minimal tensor inputs; precise shapes are not important for this behavioral test.
    t = torch.randn(2, 4, 8, dtype=torch.float32)
    cu_seqlens = torch.tensor([0, 2], dtype=torch.int32)
    freqs = torch.randn(1, 1, 4, 4, dtype=torch.float32)
    mrope_section = [0, 4, 4]

    # Call the wrapper; with fused mRoPE marked unavailable, it should take the fallback.
    _apply_rope_fp32_no_cp(
        t=t,
        cu_seqlens=cu_seqlens,
        freqs=freqs,
        mrope_section=mrope_section,
        interleaved_mrope=False,
        rotary_interleaved=False,
    )

    # Assert that the fused path was not used and the unfused RoPE path was taken.
    assert calls["fused_mrope"] == 0
    assert calls["unfused_rope"] == 1
  1. Update module_path = "examples.multimodal_dev.vision_rope_fusion" and the two monkeypatch targets (apply_fused_mrope_thd, apply_rotary_pos_emb) to match the actual module and function names used by _apply_rope_fp32_no_cp in your codebase. The key is that:
    • get_fused_mrope_thd_unavailable_reason is patched to return a non-None string.
    • The fused mRoPE THD implementation used by _apply_rope_fp32_no_cp is patched as fake_fused_apply_mrope_thd.
    • The unfused RoPE helper called by _apply_rope_fp32_no_cp is patched as fake_apply_rotary_pos_emb.
  2. If _apply_rope_fp32_no_cp requires additional parameters in your implementation (e.g., dtype, device, or vision-specific flags), pass appropriate dummy values in the test call so it exercises the exact fallback branch you want to cover.
  3. If the helper functions are imported into the module under different local names (e.g., from ... import apply_rotary_pos_emb as apply_vision_rope), adjust the monkeypatch targets to those names so the patched functions are actually used.

wplf added 2 commits May 30, 2026 12:02
…aunch path

The fused THD dispatch (rope_utils.apply_rotary_pos_emb -> fused_apply_mrope_thd)
calls the kernel directly and only validated total seqlen % cp_size, not each
packed sub-sequence. The unfused per-sequence check in _get_thd_cp_splits() is
bypassed on the fused path, so for CP>1 variable-length packing where the total
is divisible but an individual sub-sequence is not, the kernel would silently
compute wrong local->global CP token indices (global_start // cp_size).

Add the per-sequence guard in get_fused_mrope_thd_unavailable_reason (only on the
cp_size>1 path); when it triggers, the dispatch falls back to the unfused path.
…ivisibility

- Add fwd/bwd parity at the real deployment shape head_dim=256, rotary_dim=64
  (rotary_percent=0.25, 75% pass-through) with mrope_section=[11,11,10], plus the
  non-interleaved and full-rotary variants, for both BSHD and THD. The existing
  parametrized tests only covered head_dim=16/20 with rotary_dim=16 (~80% rotated).
- Add a regression test that get_fused_mrope_thd_unavailable_reason rejects a
  packed batch whose total length is CP-divisible but an individual sub-sequence
  is not (and accepts the all-divisible control).
@wplf
wplf merged commit 81921d0 into wplf:jinliangl/qwen35-vl-central-dev May 30, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants