[Minimax-M3][Perf] Use triton_mrope for vision tower + int64 offset fix for triton_mrope (#58526)
Signed-off-by: Thien Tran <gau.nernst@yahoo.com.sg> Co-authored-by: Kimi <noreply@moonshot.cn> Co-authored-by: Isotr0py <Isotr0py@outlook.com>
Thien Tran committed
Sep 25, 2026 at 09:30 UTC
16070ed7e9b051cb0b1958d4289912157026d2f4
4 files changed
+280
-179
tests/kernels/core/test_mrope.py
+73
-1
@@ -6,7 +6,10 @@ import pytest
6
import torch
7
8
from vllm.model_executor.layers.rotary_embedding import get_rope
9
-from vllm.model_executor.layers.rotary_embedding.mrope import apply_interleaved_rope
9
+from vllm.model_executor.layers.rotary_embedding.mrope import (
10
+ apply_interleaved_rope,
11
+ triton_mrope,
12
+)
13
from vllm.platforms import current_platform
14
from vllm.transformers_utils.config import get_config
15
from vllm.utils.torch_utils import set_random_seed
@@ -174,6 +177,75 @@ def test_mrope(
177
torch.testing.assert_close(query_native, query_cuda, atol=atol, rtol=rtol)
178
torch.testing.assert_close(key_native, key_cuda, atol=atol, rtol=rtol)
179
180
+ # Strided q/k views of a packed buffer (the MiniMax M3 ViT usage):
181
+ # bitwise-identical to the contiguous run, neighboring columns untouched.
182
+ pad = 16
183
+ packed = torch.zeros(
184
+ num_tokens,
185
+ query.shape[1] + key.shape[1] + pad,
186
+ dtype=dtype,
187
+ device=device,
188
+ )
189
+ packed[:, : query.shape[1]] = query
190
+ packed[:, query.shape[1] : query.shape[1] + key.shape[1]] = key
191
+ sentinel = packed[:, -pad:].clone()
192
+ q_strided = packed[:, : query.shape[1]]
193
+ k_strided = packed[:, query.shape[1] : query.shape[1] + key.shape[1]]
194
+ mrope_helper_class.forward_cuda(positions, q_strided, k_strided)
195
+ torch.testing.assert_close(q_strided, query_cuda, atol=0, rtol=0)
196
+ torch.testing.assert_close(k_strided, key_cuda, atol=0, rtol=0)
197
+ torch.testing.assert_close(packed[:, -pad:], sentinel, atol=0, rtol=0)
198
+
199
+
200
+@pytest.mark.skipif(
201
+ not current_platform.is_cuda_alike(), reason="Skipping CUDA/ROCm only tests."
202
+)
203
+def test_mrope_row_offset_int64():
204
+ # MiniMax M3 ViT worst case: a max-size video item packs ~768k patches,
205
+ # and num_tokens * packed-qkv row stride (3840) exceeds 2^31 at 559,241
206
+ # rows. Row-base math must be int64 or those rows wrap to negative
207
+ # offsets (illegal memory access at startup profiling).
208
+ set_random_seed(0)
209
+ n_qh, n_kh, hd, rd = 16, 16, 80, 78
210
+ mrope_section = [13, 13, 13]
211
+ row_stride = (n_qh + n_kh + n_kh) * hd # packed qkv row
212
+ num_tokens = 2**31 // row_stride + 8
213
+ free, _ = current_platform.mem_get_info()
214
+ if free < 2 * num_tokens * row_stride * 2:
215
+ pytest.skip("not enough free device memory")
216
+
217
+ packed = torch.empty(num_tokens, row_stride, dtype=torch.bfloat16, device=device)
218
+ q_fill = torch.randn(n_qh * hd, dtype=torch.bfloat16, device=device)
219
+ k_fill = torch.randn(n_kh * hd, dtype=torch.bfloat16, device=device)
220
+ packed[:, : n_qh * hd] = q_fill
221
+ packed[:, n_qh * hd : (n_qh + n_kh) * hd] = k_fill
222
+ half_rd = rd // 2
223
+ cos = torch.randn(3, num_tokens, half_rd, dtype=torch.float32, device=device)
224
+ sin = torch.randn(3, num_tokens, half_rd, dtype=torch.float32, device=device)
225
+ q = packed[:, : n_qh * hd]
226
+ k = packed[:, n_qh * hd : (n_qh + n_kh) * hd]
227
+ triton_mrope(q, k, cos, sin, mrope_section, hd, rd, False, True)
228
+
229
+ t_end = mrope_section[0]
230
+ h_end = t_end + mrope_section[1]
231
+ for r in (0, num_tokens - 8, num_tokens - 1):
232
+ cos_row = torch.cat(
233
+ [cos[0, r, :t_end], cos[1, r, t_end:h_end], cos[2, r, h_end:]]
234
+ )
235
+ sin_row = torch.cat(
236
+ [sin[0, r, :t_end], sin[1, r, t_end:h_end], sin[2, r, h_end:]]
237
+ )
238
+ for fill, out in ((q_fill, q[r]), (k_fill, k[r])):
239
+ row = fill.view(-1, hd).float()
240
+ r1, r2 = row[:, :half_rd], row[:, half_rd:rd]
241
+ exp = torch.cat(
242
+ [r1 * cos_row - r2 * sin_row, r2 * cos_row + r1 * sin_row, row[:, rd:]],
243
+ dim=1,
244
+ )
245
+ torch.testing.assert_close(
246
+ out, exp.view(-1).to(torch.bfloat16), atol=1e-2, rtol=1.6e-2
247
+ )
248
+
249
250
@pytest.mark.skipif(
251
not current_platform.is_cuda_alike(), reason="Skipping CUDA/ROCm only tests."
vllm/model_executor/layers/rotary_embedding/mrope.py
+12
-7
@@ -20,6 +20,8 @@ def _triton_mrope_forward(
20
cos,
21
sin,
22
num_tokens,
23
+ q_token_stride,
24
+ k_token_stride,
25
n_qh: tl.constexpr,
26
n_kh: tl.constexpr,
27
hd: tl.constexpr,
@@ -39,9 +41,10 @@ def _triton_mrope_forward(
41
# and supports cos and sin cache with shape (3, num_tokens, rotary_dim // 2)
42
# instead of (3, bsz, seq_len, head_dim), also supports interleaved rotary
43
pid = tl.program_id(0)
42
- # locate start address
43
- q_ptr = q_ptr + pid * (n_qh * hd)
44
- k_ptr = k_ptr + pid * (n_kh * hd)
44
+ # locate start address (int64: num_tokens * row_stride exceeds 2^31 for
45
+ # worst-case ViT forwards, e.g. MiniMax M3 packs ~768k patches/item)
46
+ q_ptr = q_ptr + pid.to(tl.int64) * q_token_stride
47
+ k_ptr = k_ptr + pid.to(tl.int64) * k_token_stride
48
49
# ####################################################################
50
# get the cos(mθ_{i...d/2}) and sin(mθ_{i...d/2}) for token position
@@ -186,6 +189,10 @@ def triton_mrope(
189
adjacent (GPT-J) layout.
190
191
"""
192
+ # The kernel indexes each token row through an explicit row stride and
193
+ # requires dense rows.
194
+ assert q.dim() == 2 and q.stride(-1) == 1
195
+ assert k.dim() == 2 and k.stride(-1) == 1
196
n_row, n_q_head_head_dim = q.shape
197
n_q_head = n_q_head_head_dim // head_size
198
n_kv_head = k.shape[1] // head_size
@@ -193,10 +200,6 @@ def triton_mrope(
200
pad_n_q_head = triton.next_power_of_2(n_q_head)
201
pad_n_kv_head = triton.next_power_of_2(n_kv_head)
202
196
- # ensure tensors passed into the kernel are contiguous.
197
- # It will be no-op if they are already contiguous
198
- q = q.contiguous()
199
- k = k.contiguous()
203
cos = cos.contiguous()
204
sin = sin.contiguous()
205
@@ -211,6 +214,8 @@ def triton_mrope(
214
cos,
215
sin,
216
n_row,
217
+ q.stride(0),
218
+ k.stride(0),
219
n_q_head,
220
n_kv_head,
221
head_size,
vllm/model_executor/layers/rotary_embedding/mrope_vit_setup.py
new
+153
@@ -0,0 +1,153 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+import torch
5
+
6
+from vllm.triton_utils import tl, triton
7
+from vllm.utils.torch_utils import async_tensor_h2d
8
+
9
+
10
+# num_tokens/num_grids vary per request (incl. the ==1 class) and grids_ptr
11
+# is an offset slice of the packed H2D buffer, so its 16B alignment varies
12
+# with grid count; without these the kernel recompiles mid-serving.
13
+@triton.jit(
14
+ do_not_specialize=["num_tokens", "num_grids"],
15
+ do_not_specialize_on_alignment=["grids_ptr"],
16
+)
17
+def _vit_mrope_setup_kernel(
18
+ inv_freq_t_ptr,
19
+ inv_freq_h_ptr,
20
+ inv_freq_w_ptr,
21
+ cos_out_ptr,
22
+ sin_out_ptr,
23
+ cu_seqlens_ptr, # (G + 1,) int32 token boundaries per grid
24
+ grids_ptr, # (G, 3) int32: t, h, w per grid
25
+ num_tokens,
26
+ num_grids,
27
+ half_t: tl.constexpr,
28
+ half_h: tl.constexpr,
29
+ half_w: tl.constexpr,
30
+ spatial_merge: tl.constexpr,
31
+ BLOCK_N: tl.constexpr,
32
+ BLOCK_D: tl.constexpr,
33
+):
34
+ """Build triton_mrope's (3, N, half_rot) cos/sin t/h/w planes in one
35
+ launch: compute each token's (t, h, w) position arithmetically and the
36
+ rotation in fp32 from the per-axis inv_freq. Only each axis's mrope
37
+ section columns are written; the rest is left uninitialized (the
38
+ consumer's per-axis masked loads read only the section columns)."""
39
+ pid = tl.program_id(0)
40
+ tok = pid * BLOCK_N + tl.arange(0, BLOCK_N)
41
+ tok_mask = tok < num_tokens
42
+
43
+ # Grid (segment) index per token; cu_seqlens is nondecreasing.
44
+ seg = tl.zeros([BLOCK_N], dtype=tl.int32)
45
+ for g in range(num_grids):
46
+ end = tl.load(cu_seqlens_ptr + g + 1)
47
+ seg += (tok >= end).to(tl.int32)
48
+
49
+ seg_start = tl.load(cu_seqlens_ptr + seg, mask=tok_mask, other=0)
50
+ grid_h = tl.load(grids_ptr + seg * 3 + 1, mask=tok_mask, other=1)
51
+ grid_w = tl.load(grids_ptr + seg * 3 + 2, mask=tok_mask, other=1)
52
+
53
+ local = tok - seg_start
54
+ rem = local % (grid_h * grid_w)
55
+ tpos = local // (grid_h * grid_w)
56
+ # Within a frame tokens are ordered by spatial-merge block (bh, bw, mh,
57
+ # mw), matching the reference ViT position-id permute.
58
+ bh = rem // (spatial_merge * grid_w)
59
+ r = rem % (spatial_merge * grid_w)
60
+ bw = r // (spatial_merge * spatial_merge)
61
+ r2 = r % (spatial_merge * spatial_merge)
62
+ hpos = bh * spatial_merge + r2 // spatial_merge
63
+ wpos = bw * spatial_merge + r2 % spatial_merge
64
+
65
+ d = tl.arange(0, BLOCK_D)
66
+ half_rot = half_t + half_h + half_w
67
+ row = (tok * half_rot)[:, None] + d[None, :]
68
+ plane = num_tokens * half_rot
69
+
70
+ # Per-axis compute+store to cap live registers; the fp32 trig sequence
71
+ # wants few elements per thread (hence small BLOCK_N, many warps).
72
+ in_t = d < half_t
73
+ inv_t = tl.load(inv_freq_t_ptr + d, mask=in_t, other=0.0)
74
+ freq = tpos.to(tl.float32)[:, None] * inv_t[None, :]
75
+ mask = tok_mask[:, None] & in_t[None, :]
76
+ tl.store(cos_out_ptr + row, tl.cos(freq), mask=mask)
77
+ tl.store(sin_out_ptr + row, tl.sin(freq), mask=mask)
78
+
79
+ in_h = (d >= half_t) & (d < half_t + half_h)
80
+ inv_h = tl.load(inv_freq_h_ptr + d - half_t, mask=in_h, other=0.0)
81
+ freq = hpos.to(tl.float32)[:, None] * inv_h[None, :]
82
+ mask = tok_mask[:, None] & in_h[None, :]
83
+ tl.store(cos_out_ptr + plane + row, tl.cos(freq), mask=mask)
84
+ tl.store(sin_out_ptr + plane + row, tl.sin(freq), mask=mask)
85
+
86
+ in_w = (d >= half_t + half_h) & (d < half_rot)
87
+ inv_w = tl.load(inv_freq_w_ptr + d - half_t - half_h, mask=in_w, other=0.0)
88
+ freq = wpos.to(tl.float32)[:, None] * inv_w[None, :]
89
+ mask = tok_mask[:, None] & in_w[None, :]
90
+ tl.store(cos_out_ptr + 2 * plane + row, tl.cos(freq), mask=mask)
91
+ tl.store(sin_out_ptr + 2 * plane + row, tl.sin(freq), mask=mask)
92
+
93
+
94
+def vit_mrope_setup(
95
+ inv_freq_t: torch.Tensor,
96
+ inv_freq_h: torch.Tensor,
97
+ inv_freq_w: torch.Tensor,
98
+ grid_thw: list[list[int]],
99
+ spatial_merge_size: int,
100
+) -> tuple[torch.Tensor, torch.Tensor]:
101
+ """(3, total_N, half_rot_dim) fp32 cos/sin t/h/w planes for triton_mrope.
102
+
103
+ One fused kernel: positions are computed arithmetically from the grids
104
+ and the rotation comes from the per-axis inv_freq, so there is no
105
+ cos/sin cache to size or grow.
106
+
107
+ Args:
108
+ inv_freq_t: t-axis inverse frequencies, (t_dim // 2,).
109
+ inv_freq_h: h-axis inverse frequencies, (h_dim // 2,).
110
+ inv_freq_w: w-axis inverse frequencies, (w_dim // 2,).
111
+ grid_thw: per-grid [t, h, w] patch counts (host metadata).
112
+ spatial_merge_size: spatial merge factor of the ViT.
113
+
114
+ """
115
+ device = inv_freq_t.device
116
+ cu = [0]
117
+ for t, h, w in grid_thw:
118
+ cu.append(cu[-1] + t * h * w)
119
+ n = cu[-1]
120
+ g = len(grid_thw)
121
+ # grid_thw is per-request host metadata, so one H2D is unavoidable;
122
+ # pack cu_seqlens and grids into a single pinned async copy.
123
+ buf = async_tensor_h2d(
124
+ cu + [d for grid in grid_thw for d in grid], device=device, dtype=torch.int32
125
+ )
126
+ cu_seqlens = buf[: g + 1]
127
+ grids = buf[g + 1 :].view(g, 3)
128
+
129
+ half_t = inv_freq_t.numel()
130
+ half_h = inv_freq_h.numel()
131
+ half_w = inv_freq_w.numel()
132
+ half_rot = half_t + half_h + half_w
133
+ cos = torch.empty(3, n, half_rot, device=device, dtype=torch.float32)
134
+ sin = torch.empty(3, n, half_rot, device=device, dtype=torch.float32)
135
+ _vit_mrope_setup_kernel[(triton.cdiv(n, 32),)](
136
+ inv_freq_t,
137
+ inv_freq_h,
138
+ inv_freq_w,
139
+ cos,
140
+ sin,
141
+ cu_seqlens,
142
+ grids,
143
+ n,
144
+ g,
145
+ half_t=half_t,
146
+ half_h=half_h,
147
+ half_w=half_w,
148
+ spatial_merge=spatial_merge_size,
149
+ BLOCK_N=32,
150
+ BLOCK_D=triton.next_power_of_2(half_rot),
151
+ num_warps=8,
152
+ )
153
+ return cos, sin
vllm/models/minimax_m3/common/vision_tower.py
+42
-171
@@ -21,20 +21,16 @@ from vllm.model_executor.layers.linear import (
21
RowParallelLinear,
22
)
23
from vllm.model_executor.layers.quantization import QuantizationConfig
24
-from vllm.model_executor.layers.rotary_embedding.common import ApplyRotaryEmb
24
+from vllm.model_executor.layers.rotary_embedding.mrope import triton_mrope
25
+from vllm.model_executor.layers.rotary_embedding.mrope_vit_setup import (
26
+ vit_mrope_setup,
27
+)
28
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
29
from vllm.model_executor.models.utils import maybe_prefix
30
from vllm.model_executor.models.vision import (
31
get_vit_attn_backend,
32
is_vit_use_data_parallel,
33
)
31
-from vllm.platforms import current_platform
32
-
33
-# ROCm caps a kernel-launch gridDim.y at 65536. The HIP flash-attn Triton
34
-# rotary kernel launches grid.y = cdiv(seqlen, BLOCK_M), so it fails with
35
-# hipErrorInvalidValue once cdiv(seqlen, BLOCK_M) > 65536. Used below to decide
36
-# when RoPE must be applied per video segment instead of in one launch.
37
-_HIP_MAX_GRID_DIM_Y = 65536
34
35
36
class MiniMaxVLPatchEmbed(nn.Module):
@@ -125,56 +121,13 @@ class MiniMaxVLAttention(nn.Module):
121
head_size=self.head_dim,
122
prefix=f"{prefix}.attn",
123
)
128
- # ApplyRotaryEmb handles the internal cos/sin repeat and partial
129
- # rotation (ro_dim = half_rot_dim * 2 < head_dim for MiniMax).
130
- # enable_fp32_compute=True runs the rotation in fp32 (q/k upcast,
131
- # fp32 cos/sin), matching the reference ``_minimax_rope_applier``.
132
- self.apply_rotary_emb = ApplyRotaryEmb(
133
- enforce_enable=True, enable_fp32_compute=True
134
- )
135
-
136
- def _apply_rotary_emb(
137
- self,
138
- qk_reshaped: torch.Tensor,
139
- rotary_cos: torch.Tensor,
140
- rotary_sin: torch.Tensor,
141
- seq_len: int,
142
- rotary_segment_lengths: list[int] | None,
143
- ) -> torch.Tensor:
144
- # Default fast path (all NVIDIA inputs, and ROCm short clips/images):
145
- # a single rotary kernel launch. ``rotary_segment_lengths`` is only
146
- # populated on ROCm (see ``MiniMaxVLVisionTransformer.forward``), so
147
- # the per-segment path below is ROCm-only and never touches the
148
- # NVIDIA/CUDA code path.
149
- if not current_platform.is_rocm() or rotary_segment_lengths is None:
150
- return self.apply_rotary_emb(qk_reshaped, rotary_cos, rotary_sin)
151
-
152
- # ROCm only: the HIP flash-attn Triton rotary kernel fails with
153
- # hipErrorInvalidValue once grid.y = cdiv(seqlen, BLOCK_M) exceeds
154
- # _HIP_MAX_GRID_DIM_Y (65536). BLOCK_M is 8 for rotary_dim <= 128
155
- # (MiniMax-M3 vision: rotary_dim=78), giving a hard limit of
156
- # 65536 * BLOCK_M tokens — measured exactly as 524288 OK / 524289 fail.
157
- # Only long videos cross it; since vision_segment_max_frames caps each
158
- # segment at a few frames (<< limit), applying RoPE per segment keeps
159
- # every sub-call in range. Splitting on segment boundaries is
160
- # mathematically exact because rotary_cos/sin are precomputed per token.
161
- # Images and short clips stay on the single-kernel fast path above.
162
- rotary_dim = rotary_cos.shape[-1] * 2
163
- block_m = 8 if rotary_dim <= 128 else 4
164
- hip_rotary_max_seqlen = _HIP_MAX_GRID_DIM_Y * block_m
165
- if seq_len <= hip_rotary_max_seqlen or len(rotary_segment_lengths) <= 1:
166
- return self.apply_rotary_emb(qk_reshaped, rotary_cos, rotary_sin)
167
-
168
- qk_segments = qk_reshaped.split(rotary_segment_lengths, dim=1)
169
- cos_segments = rotary_cos.split(rotary_segment_lengths, dim=0)
170
- sin_segments = rotary_sin.split(rotary_segment_lengths, dim=0)
171
- return torch.cat(
172
- [
173
- self.apply_rotary_emb(qk_s, cos_s, sin_s)
174
- for qk_s, cos_s, sin_s in zip(qk_segments, cos_segments, sin_segments)
175
- ],
176
- dim=1,
177
- )
124
+ # Partial 3D RoPE geometry: rot_dim is split evenly across t/h/w
125
+ # (same formula as MiniMaxVLVisionTransformer; rot_dim may be <
126
+ # head_dim). mrope_section is in half-dim units for triton_mrope.
127
+ rope_dims = 2 * (self.head_dim // 2)
128
+ axis_dim = 2 * ((rope_dims // 3) // 2)
129
+ self.rotary_dim = 3 * axis_dim
130
+ self.mrope_section = [axis_dim // 2] * 3
131
132
def forward(
133
self,
@@ -183,33 +136,34 @@ class MiniMaxVLAttention(nn.Module):
136
rotary_cos: torch.Tensor,
137
rotary_sin: torch.Tensor,
138
max_seqlen: torch.Tensor,
186
- rotary_segment_lengths: list[int] | None = None,
139
sequence_lengths: torch.Tensor | None = None,
140
) -> torch.Tensor:
141
# x: (N, 1, embed_dim) [seq=N, batch=1, chan=embed_dim]
142
x_qkv, _ = self.qkv_proj(x) # (N, 1, 3 * heads_per_part * head_dim)
143
seq_len, batch_size, _ = x_qkv.shape
144
193
- # Rearrange to (b=1, N, 3, heads, head_dim) — same as Qwen2_5_VisionAttention
194
- qkv = rearrange(
195
- x_qkv,
196
- "s b (three head d) -> b s three head d",
197
- three=3,
198
- head=self.num_heads_per_partition,
199
- )
200
- qk, v = qkv[:, :, :2], qkv[:, :, 2] # (b,N,2,h,d) and (b,N,h,d)
201
-
202
- # Stack q/k → (2*b, N, heads, head_dim) for joint RoPE application.
203
- # rotary_cos/sin: (N, half_rot_dim) — ApplyRotaryEmb expands internally
204
- # and rotates only the first 2*half_rot_dim dims, passing the rest through.
205
- qk_reshaped = rearrange(qk, "b s two h d -> (two b) s h d", two=2).contiguous()
206
- qk_rotated = self._apply_rotary_emb(
207
- qk_reshaped, rotary_cos, rotary_sin, seq_len, rotary_segment_lengths
208
- )
209
- qk_rotated = qk_rotated.view(
210
- 2, batch_size, seq_len, self.num_heads_per_partition, self.head_dim
145
+ qkv = x_qkv.view(seq_len, 3, self.num_heads_per_partition, self.head_dim)
146
+ v = qkv[:, 2].unsqueeze(0) # (b=1, N, heads, head_dim) strided view
147
+
148
+ # In-place partial 3D RoPE on strided q/k views of the packed qkv
149
+ # tensor: one kernel launch, no repack/upcast copies. fp32 cos/sin
150
+ # make the kernel compute in fp32, matching the reference
151
+ # ``_minimax_rope_applier`` precision.
152
+ q = qkv[:, 0].reshape(seq_len, -1)
153
+ k = qkv[:, 1].reshape(seq_len, -1)
154
+ triton_mrope(
155
+ q,
156
+ k,
157
+ rotary_cos,
158
+ rotary_sin,
159
+ self.mrope_section,
160
+ self.head_dim,
161
+ self.rotary_dim,
162
+ mrope_interleaved=False,
163
+ is_neox_style=True,
164
)
212
- q, k = qk_rotated.unbind(dim=0) # each (b=1, N, heads, head_dim)
165
+ q = q.view(batch_size, seq_len, self.num_heads_per_partition, self.head_dim)
166
+ k = k.view(batch_size, seq_len, self.num_heads_per_partition, self.head_dim)
167
168
# Flash attention → (b, N, heads, head_dim)
169
context = self.attn(
@@ -272,7 +226,6 @@ class MiniMaxVLEncoderLayer(nn.Module):
226
rotary_cos: torch.Tensor,
227
rotary_sin: torch.Tensor,
228
max_seqlen: torch.Tensor,
275
- rotary_segment_lengths: list[int] | None = None,
229
sequence_lengths: torch.Tensor | None = None,
230
) -> torch.Tensor:
231
# x: (N, 1, hidden_size)
@@ -282,7 +235,6 @@ class MiniMaxVLEncoderLayer(nn.Module):
235
rotary_cos,
236
rotary_sin,
237
max_seqlen,
285
- rotary_segment_lengths,
238
sequence_lengths,
239
)
240
residual = x
@@ -324,7 +276,6 @@ class MiniMaxVLEncoder(nn.Module):
276
rotary_cos: torch.Tensor,
277
rotary_sin: torch.Tensor,
278
max_seqlen: torch.Tensor,
327
- rotary_segment_lengths: list[int] | None = None,
279
sequence_lengths: torch.Tensor | None = None,
280
) -> torch.Tensor:
281
for layer in self.layers:
@@ -334,7 +285,6 @@ class MiniMaxVLEncoder(nn.Module):
285
rotary_cos,
286
rotary_sin,
287
max_seqlen,
337
- rotary_segment_lengths,
288
sequence_lengths,
289
)
290
return x
@@ -427,82 +377,6 @@ class MiniMaxVLVisionTransformer(nn.Module):
377
# out_hidden_size needed by run_dp_sharded_mrope_vision_model
378
self.out_hidden_size = embed_dim
379
430
- # ── RoPE helpers ─────────────────────────────────────────────────────
431
-
432
- def _get_3d_rope_embed(
433
- self, grid_t: int, grid_h: int, grid_w: int, spatial_merge_size: int
434
- ) -> torch.Tensor:
435
- """Compute 3D RoPE frequencies for a single (T, H, W) grid.
436
-
437
- Returns (T*H*W, half_rot_dim) on the same device as inv_freq buffers.
438
- Mirrors the reference ``_get_3d_rope_embed`` exactly.
439
- """
440
- tokens_per_frame = grid_h * grid_w
441
-
442
- tpos_ids = (
443
- torch.arange(grid_t, device=self.inv_freq_t.device)
444
- .unsqueeze(1)
445
- .expand(-1, tokens_per_frame)
446
- .flatten()
447
- )
448
-
449
- hpos_ids = (
450
- torch.arange(grid_h, device=self.inv_freq_h.device)
451
- .unsqueeze(1)
452
- .expand(-1, grid_w)
453
- .reshape(
454
- grid_h // spatial_merge_size,
455
- spatial_merge_size,
456
- grid_w // spatial_merge_size,
457
- spatial_merge_size,
458
- )
459
- .permute(0, 2, 1, 3)
460
- .unsqueeze(0)
461
- .expand(grid_t, -1, -1, -1, -1)
462
- .flatten()
463
- )
464
- wpos_ids = (
465
- torch.arange(grid_w, device=self.inv_freq_w.device)
466
- .unsqueeze(0)
467
- .expand(grid_h, -1)
468
- .reshape(
469
- grid_h // spatial_merge_size,
470
- spatial_merge_size,
471
- grid_w // spatial_merge_size,
472
- spatial_merge_size,
473
- )
474
- .permute(0, 2, 1, 3)
475
- .unsqueeze(0)
476
- .expand(grid_t, -1, -1, -1, -1)
477
- .flatten()
478
- )
479
-
480
- max_t = max(grid_t, 1)
481
- max_hw = max(grid_h, grid_w)
482
-
483
- seq_t = torch.arange(
484
- max_t, device=self.inv_freq_t.device, dtype=self.inv_freq_t.dtype
485
- )
486
- seq_hw = torch.arange(
487
- max_hw, device=self.inv_freq_h.device, dtype=self.inv_freq_h.dtype
488
- )
489
-
490
- freqs_t = torch.outer(seq_t, self.inv_freq_t) # (max_t, t_dim/2)
491
- freqs_h = torch.outer(seq_hw, self.inv_freq_h) # (max_hw, h_dim/2)
492
- freqs_w = torch.outer(seq_hw, self.inv_freq_w) # (max_hw, w_dim/2)
493
-
494
- return torch.cat(
495
- [freqs_t[tpos_ids], freqs_h[hpos_ids], freqs_w[wpos_ids]], dim=-1
496
- ) # (T*H*W, half_rot_dim)
497
-
498
- def _get_rope_embed_3d(
499
- self, grid_thw: list[list[int]], spatial_merge_size: int
500
- ) -> torch.Tensor:
501
- embeds = [
502
- self._get_3d_rope_embed(t, h, w, spatial_merge_size) for t, h, w in grid_thw
503
- ]
504
- return torch.cat(embeds, dim=0) # (total_N, half_rot_dim)
505
-
380
# ── Frame-limit helper (mirrors the reference) ───────────────────────
381
382
def _apply_max_frames_limit(self, grid_thw: list[list[int]]) -> list[list[int]]:
@@ -557,21 +431,19 @@ class MiniMaxVLVisionTransformer(nn.Module):
431
hidden.device,
432
)
433
560
- # 3D RoPE: (total_N, half_rot_dim); ApplyRotaryEmb expands internally
561
- freqs = self._get_rope_embed_3d(limited, self.spatial_merge_size)
562
- freqs = freqs.to(device=hidden.device)
563
- # Keep cos/sin in fp32; ApplyRotaryEmb(enable_fp32_compute=True) runs the
564
- # rotation in fp32 to match the reference precision.
565
- rotary_cos, rotary_sin = freqs.cos(), freqs.sin()
434
+ # 3D RoPE cos/sin: (3, total_N, half_rot_dim) fp32 t/h/w planes for
435
+ # triton_mrope from one fused setup kernel. Kept fp32 so the kernel
436
+ # computes the rotation in fp32, matching the reference.
437
+ rotary_cos, rotary_sin = vit_mrope_setup(
438
+ self.inv_freq_t,
439
+ self.inv_freq_h,
440
+ self.inv_freq_w,
441
+ limited,
442
+ self.spatial_merge_size,
443
+ )
444
445
# Encoder expects (N, 1, hidden_size) — add batch dim
446
hidden = hidden.unsqueeze(1)
569
- # On ROCm, the flash_attn Triton rotary kernel can fail with
570
- # hipErrorInvalidValue when seqlen is very large, e.g. 192k video
571
- # tokens; pass per-segment lengths so RoPE can be applied in chunks.
572
- # On other platforms leave it None -> single-kernel fast path, so the
573
- # NVIDIA/CUDA code path is unchanged.
574
- rotary_segment_lengths = lens if current_platform.is_rocm() else None
447
448
hidden = self.encoder(
449
hidden,
@@ -579,7 +451,6 @@ class MiniMaxVLVisionTransformer(nn.Module):
451
rotary_cos,
452
rotary_sin,
453
max_seqlen,
582
- rotary_segment_lengths,
454
sequence_lengths=sequence_lengths,
455
)
456
hidden = hidden.squeeze(1) # back to (total_N, hidden_size)