66from functools import lru_cache
77from typing import Any , Dict , List , Optional , Tuple , Union
88
9+ import numpy as np
910import torch
1011import torch .nn as nn
1112from torch .nn import functional as F
5051# the pattern used in `custom_ops` itself.
5152if IS_FLASHINFER_AVAILABLE :
5253 from ..custom_ops import flashinfer_apply_rope_with_cos_sin_cache_inplace
54+
55+ # Vision RoPE on Qwen2-VL/2.5-VL/3-VL uses head_dim 72/80 which doesn't satisfy
56+ # FlashInfer's "head_size % 64 == 0" precondition. Fall back to flash_attn's
57+ # fused Triton rotary kernel: one launch instead of the ~7 elementwise launches
58+ # of the PyTorch path (mul x4, add/sub x2, cat x1).
59+ try :
60+ from flash_attn .ops .triton .rotary import \
61+ apply_rotary as _flash_attn_apply_rotary # type: ignore
62+ _FLASH_ATTN_ROTARY_AVAILABLE = True
63+ except ImportError : # pragma: no cover - flash_attn is part of the default deps
64+ _flash_attn_apply_rotary = None
65+ _FLASH_ATTN_ROTARY_AVAILABLE = False
5366from ..modules .gated_mlp import GatedMLP
5467from ..modules .rotary_embedding import MRotaryEmbedding , RotaryEmbedding
5568from .modeling_auto import AutoModelForCausalLM
@@ -147,7 +160,6 @@ def get_rope_index(
147160 image_token_id = config .image_token_id
148161 video_token_id = config .video_token_id
149162 vision_start_token_id = config .vision_start_token_id
150- mrope_position_deltas = []
151163
152164 # Handle case with no vision inputs
153165 if image_grid_thw is None and video_grid_thw is None :
@@ -172,121 +184,124 @@ def get_rope_index(
172184 )
173185 return position_ids , mrope_position_deltas
174186
175- # Handle case with vision inputs
176- total_input_ids = input_ids
177- if attention_mask is None :
178- attention_mask = torch .ones_like (total_input_ids )
179-
180- position_ids = torch .ones (
181- 3 ,
182- input_ids .shape [0 ],
183- input_ids .shape [1 ],
184- dtype = input_ids .dtype ,
185- device = input_ids .device ,
186- )
187-
188- image_index , video_index = 0 , 0
189- attention_mask = attention_mask .to (total_input_ids .device )
190-
191- for i , input_ids in enumerate (total_input_ids ):
192- input_ids = input_ids [attention_mask [i ] == 1 ]
193- image_nums , video_nums = 0 , 0
194- vision_start_indices = torch .argwhere (
195- input_ids == vision_start_token_id ).squeeze (1 )
196- vision_tokens = input_ids [vision_start_indices + 1 ]
197- image_nums = (vision_tokens == image_token_id ).sum ()
198- video_nums = (vision_tokens == video_token_id ).sum ()
199- input_tokens = input_ids .tolist ()
200- llm_pos_ids_list : list = []
187+ # numpy-based vectorized impl: ~2-3× faster than the torch loop
188+ # version on CPU since per-image/video work is small tensor
189+ # allocations dominated by Python+dispatch overhead. Output tensors
190+ # are placed back on ``input_ids.device``.
191+ input_device = input_ids .device
192+ input_dtype = input_ids .dtype
193+ ids_np = input_ids .detach ().cpu ().numpy ()
194+ attn_np = (np .ones_like (ids_np ) if attention_mask is None else
195+ attention_mask .detach ().cpu ().numpy ())
196+ image_grid_np = (image_grid_thw .detach ().cpu ().numpy ()
197+ if image_grid_thw is not None else None )
198+ video_grid_np = (video_grid_thw .detach ().cpu ().numpy ()
199+ if video_grid_thw is not None else None )
200+ if second_per_grid_ts is not None and isinstance (
201+ second_per_grid_ts , torch .Tensor ):
202+ second_per_grid_ts_np = second_per_grid_ts .detach ().cpu ().numpy ()
203+ else :
204+ second_per_grid_ts_np = second_per_grid_ts
205+ tokens_per_second = (config .vision_config .tokens_per_second if hasattr (
206+ config .vision_config , 'tokens_per_second' ) else None )
207+ B , S = ids_np .shape
208+ position_ids_np = np .ones ((3 , B , S ), dtype = ids_np .dtype )
209+ deltas = []
210+ image_index = 0
211+ video_index = 0
212+ for i in range (B ):
213+ mask_i = attn_np [i ] == 1
214+ seq = ids_np [i ][mask_i ]
215+ vision_start_indices = np .flatnonzero (seq == vision_start_token_id )
216+ vision_tokens = (seq [vision_start_indices +
217+ 1 ] if vision_start_indices .size else seq [:0 ])
218+ image_nums = int ((vision_tokens == image_token_id ).sum ())
219+ video_nums = int ((vision_tokens == video_token_id ).sum ())
220+ seq_list = seq .tolist ()
221+ has_image = image_token_id in seq_list
222+ has_video = video_token_id in seq_list
223+ llm_pos_ids_list = []
201224 st = 0
202225 remain_images , remain_videos = image_nums , video_nums
203-
204226 for _ in range (image_nums + video_nums ):
205- if image_token_id in input_tokens and remain_images > 0 :
206- ed_image = input_tokens .index (image_token_id , st )
227+ if has_image and remain_images > 0 :
228+ ed_image = seq_list .index (image_token_id , st )
207229 else :
208- ed_image = len (input_tokens ) + 1
209- if video_token_id in input_tokens and remain_videos > 0 :
210- ed_video = input_tokens .index (video_token_id , st )
230+ ed_image = len (seq_list ) + 1
231+ if has_video and remain_videos > 0 :
232+ ed_video = seq_list .index (video_token_id , st )
211233 else :
212- ed_video = len (input_tokens ) + 1
213-
234+ ed_video = len (seq_list ) + 1
214235 if ed_image < ed_video :
215- t , h , w = (
216- image_grid_thw [image_index ][0 ],
217- image_grid_thw [image_index ][1 ],
218- image_grid_thw [image_index ][2 ],
219- )
236+ t = int (image_grid_np [image_index ][0 ])
237+ h = int (image_grid_np [image_index ][1 ])
238+ w = int (image_grid_np [image_index ][2 ])
220239 second_per_grid_t = 0
221240 image_index += 1
222241 remain_images -= 1
223242 ed = ed_image
224243 else :
225- t , h , w = (
226- video_grid_thw [video_index ][0 ],
227- video_grid_thw [video_index ][1 ],
228- video_grid_thw [video_index ][2 ],
229- )
230- if second_per_grid_ts is not None :
231- second_per_grid_t = second_per_grid_ts [video_index ]
244+ t = int (video_grid_np [video_index ][0 ])
245+ h = int (video_grid_np [video_index ][1 ])
246+ w = int (video_grid_np [video_index ][2 ])
247+ if second_per_grid_ts_np is not None :
248+ second_per_grid_t = float (
249+ second_per_grid_ts_np [video_index ])
232250 else :
233251 second_per_grid_t = 1.0
234252 video_index += 1
235253 remain_videos -= 1
236254 ed = ed_video
237-
238- llm_grid_t , llm_grid_h , llm_grid_w = (
239- t .item (),
240- h .item () // spatial_merge_size ,
241- w .item () // spatial_merge_size ,
242- )
255+ llm_grid_t = t
256+ llm_grid_h = h // spatial_merge_size
257+ llm_grid_w = w // spatial_merge_size
243258 text_len = ed - st
244-
245- st_idx = llm_pos_ids_list [ - 1 ]. max ( ) + 1 if len (
246- llm_pos_ids_list ) > 0 else 0
247- llm_pos_ids_list .append (
248- torch . arange (text_len ). view ( 1 , - 1 ). expand (3 , - 1 ) + st_idx )
249-
259+ st_idx = llm_pos_ids_list [ - 1 ]. max (
260+ ) + 1 if llm_pos_ids_list else 0
261+ if text_len > 0 :
262+ llm_pos_ids_list .append (
263+ np . broadcast_to ( np . arange (text_len ), (3 , text_len )) +
264+ st_idx )
250265 # Calculate temporal position IDs based on model type
251- if hasattr ( config . vision_config , ' tokens_per_second' ) :
266+ if tokens_per_second is not None :
252267 # Qwen2_5_VL style temporal position calculation
253- if isinstance (second_per_grid_t , torch .Tensor ):
254- second_per_grid_t = second_per_grid_t .item ()
255- range_tensor = torch .arange (llm_grid_t ).view (- 1 , 1 )
256- expanded_range = range_tensor .expand (
257- - 1 , llm_grid_h * llm_grid_w )
258- time_tensor = expanded_range * second_per_grid_t * config .vision_config .tokens_per_second
259- t_index = time_tensor .long ().flatten ()
268+ t_index = (np .arange (llm_grid_t ).reshape (- 1 , 1 ) *
269+ (second_per_grid_t * tokens_per_second ))
270+ t_index = np .broadcast_to (
271+ t_index .astype (np .int64 ),
272+ (llm_grid_t , llm_grid_h * llm_grid_w ),
273+ ).reshape (- 1 )
274+ h_idx = np .broadcast_to (
275+ np .arange (llm_grid_h ).reshape (1 , - 1 , 1 ),
276+ (llm_grid_t , llm_grid_h , llm_grid_w ),
277+ ).reshape (- 1 )
278+ w_idx = np .broadcast_to (
279+ np .arange (llm_grid_w ).reshape (1 , 1 , - 1 ),
280+ (llm_grid_t , llm_grid_h , llm_grid_w ),
281+ ).reshape (- 1 )
282+ block = np .stack ([t_index , h_idx , w_idx ])
260283 else :
261284 # Qwen2VL style temporal position calculation
262- t_index = torch .arange (llm_grid_t ).view (- 1 , 1 ).expand (
263- - 1 , llm_grid_h * llm_grid_w ).flatten ()
264-
265- h_index = torch .arange (llm_grid_h ).view (1 , - 1 , 1 ).expand (
266- llm_grid_t , - 1 , llm_grid_w ).flatten ()
267- w_index = torch .arange (llm_grid_w ).view (1 , 1 , - 1 ).expand (
268- llm_grid_t , llm_grid_h , - 1 ).flatten ()
269-
270- llm_pos_ids_list .append (
271- torch .stack ([t_index , h_index , w_index ]) + text_len +
272- st_idx )
285+ block = np .indices (
286+ (llm_grid_t , llm_grid_h , llm_grid_w )).reshape (3 , - 1 )
287+ llm_pos_ids_list .append (block + text_len + st_idx )
273288 st = ed + llm_grid_t * llm_grid_h * llm_grid_w
274-
275- if st < len (input_tokens ):
276- st_idx = llm_pos_ids_list [- 1 ].max () + 1 if len (
277- llm_pos_ids_list ) > 0 else 0
278- text_len = len (input_tokens ) - st
289+ if st < len (seq_list ):
290+ st_idx = llm_pos_ids_list [- 1 ].max (
291+ ) + 1 if llm_pos_ids_list else 0
292+ text_len = len (seq_list ) - st
279293 llm_pos_ids_list .append (
280- torch .arange (text_len ).view (1 , - 1 ).expand (3 , - 1 ) + st_idx )
281-
282- llm_positions = torch .cat (llm_pos_ids_list , dim = 1 ).reshape (3 , - 1 )
283- position_ids [..., i , attention_mask [i ] == 1 ] = llm_positions .to (
284- position_ids .device )
285- mrope_position_deltas .append (llm_positions .max () + 1 -
286- len (total_input_ids [i ]))
287-
288- mrope_position_deltas = torch .tensor (
289- mrope_position_deltas , device = input_ids .device ).unsqueeze (1 )
294+ np .broadcast_to (np .arange (text_len ), (3 , text_len )) +
295+ st_idx )
296+ llm_positions = np .concatenate (llm_pos_ids_list ,
297+ axis = 1 ).reshape (3 , - 1 )
298+ position_ids_np [:, i , mask_i ] = llm_positions
299+ deltas .append (
300+ int (llm_positions .max ()) + 1 - int (ids_np [i ].shape [0 ]))
301+ position_ids = torch .from_numpy (position_ids_np ).to (device = input_device ,
302+ dtype = input_dtype )
303+ mrope_position_deltas = torch .tensor (deltas ,
304+ device = input_device ).unsqueeze (1 )
290305 return position_ids , mrope_position_deltas
291306
292307 def _preprocess (self , text : Dict [str , any ], mm_data : Dict [str , any ],
@@ -645,13 +660,39 @@ def apply_rope(self,
645660 err ,
646661 )
647662
648- cos = cos .to (dtype = q .dtype )
649- sin = sin .to (dtype = q .dtype )
663+ # cos/sin are pre-cast to ``q.dtype`` upstream (``get_rope_and_window_
664+ # index_by_thw`` for Qwen2.5-VL; ``rope_cos_cache`` buffer dtype for
665+ # Qwen3-VL). When that holds, ``.to(dtype=q.dtype)`` is a no-op; keep
666+ # it as a safety net for callers/quantization paths that didn't pre-cast.
667+ if cos .dtype != q .dtype :
668+ cos = cos .to (dtype = q .dtype )
669+ if sin .dtype != q .dtype :
670+ sin = sin .to (dtype = q .dtype )
650671 q = q .view (seq_len , - 1 , self .head_dim )
651672 k = k .view (seq_len , - 1 , self .head_dim )
652673 v = v .view (seq_len , - 1 , self .head_dim )
653- q = RotaryEmbedding .apply_rotary_pos_emb (q , cos , sin )
654- k = RotaryEmbedding .apply_rotary_pos_emb (k , cos , sin )
674+ if _FLASH_ATTN_ROTARY_AVAILABLE :
675+ # flash_attn Triton kernel: single launch per tensor. cos/sin
676+ # are expected as ``[seqlen, head_dim/2]``. The PyTorch path
677+ # built ``RotaryEmbedding.apply_rotary_pos_emb`` with cos/sin
678+ # already in that layout (see ``get_rotary_pos_emb_window_data``).
679+ # The kernel takes 4D ``(batch, seq, nheads, headdim)``; add a
680+ # batch dim, run in-place, then drop it.
681+ q4 = q .unsqueeze (0 )
682+ k4 = k .unsqueeze (0 )
683+ _flash_attn_apply_rotary (q4 ,
684+ cos ,
685+ sin ,
686+ interleaved = False ,
687+ inplace = True )
688+ _flash_attn_apply_rotary (k4 ,
689+ cos ,
690+ sin ,
691+ interleaved = False ,
692+ inplace = True )
693+ else :
694+ q = RotaryEmbedding .apply_rotary_pos_emb (q , cos , sin )
695+ k = RotaryEmbedding .apply_rotary_pos_emb (k , cos , sin )
655696 q , k , v = q .reshape (seq_len , - 1 ), k .reshape (seq_len ,
656697 - 1 ), v .reshape (seq_len , - 1 )
657698 return q , k , v
@@ -732,21 +773,21 @@ def forward(
732773 position_embeddings : Optional [Tuple [torch .Tensor , torch .Tensor ]] = None ,
733774 ** kwargs ,
734775 ) -> torch .Tensor :
735-
736- residual = hidden_states
737- hidden_states = self .norm1 (hidden_states )
738- hidden_states = residual + self .attn (
739- hidden_states = hidden_states ,
776+ # vLLM-style residual fusion: collapse the post-attn residual add
777+ # into ``norm2``'s residual path (``RMSNorm.forward`` with ``residual=``
778+ # uses the FlashInfer ``fused_add_rmsnorm`` kernel). Saves one
779+ # ``add<c10::BFloat16>`` launch per vision block (= 32 launches per
780+ # executor iter at full-batch). Mirrors
781+ # ``vllm/model_executor/models/qwen2_5_vl.py::Qwen2_5_VisionBlock``.
782+ x_attn = self .attn (
783+ hidden_states = self .norm1 (hidden_states ),
740784 attn_metadata = attn_metadata ,
741785 position_ids = position_ids ,
742786 position_embeddings = position_embeddings ,
743787 ** kwargs ,
744788 )
745-
746- residual = hidden_states
747- hidden_states = self .norm2 (hidden_states )
748- hidden_states = residual + self .mlp (hidden_states )
749- return hidden_states
789+ x_fused_norm , residual = self .norm2 (hidden_states , residual = x_attn )
790+ return residual + self .mlp (x_fused_norm )
750791
751792
752793class Qwen2_5_VLPatchMerger (torch .nn .Module ):
@@ -989,6 +1030,18 @@ def get_rope_and_window_index_by_thw(
9891030 sin_thw = sin_thw [window_index_thw_dev , :, :].reshape (
9901031 - 1 , sin_thw .shape [- 1 ])
9911032
1033+ # Cast once (per cached (t, h, w)) to the vision-tower dtype so the
1034+ # per-block ``cos.to(dtype=q.dtype)`` cast in ``apply_rope`` becomes a
1035+ # no-op. Mirrors vLLM's ``get_rope_by_thw`` which builds the rotary
1036+ # cache directly in the model dtype (see note above).
1037+ target_dtype = (
1038+ self .model_config .pretrained_config .vision_config .torch_dtype
1039+ if hasattr (self .model_config .pretrained_config .vision_config ,
1040+ "torch_dtype" ) else
1041+ self .model_config .pretrained_config .torch_dtype )
1042+ cos_thw = cos_thw .to (target_dtype )
1043+ sin_thw = sin_thw .to (target_dtype )
1044+
9921045 return cos_thw , sin_thw , window_index_thw , tuple (seq_lens_thw )
9931046
9941047 def prepare_attn_metadata (self , batch_size : int , seq_lens : List [int ],
0 commit comments