Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/fast_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
matrix:
os: ["ubuntu-latest"]
python-version: ["3.13"]
transformers-version: ["4.57", "5.6", "5.9"]
transformers-version: ["4.57", "5.6", "5.12.1"]
torch:
- version: "2.12.0"
steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/fast_tests_ort_nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
run: pip install "torch==${{ matrix.torch.version }}+cpu" torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu

- name: Install dependencies
run: pip install -e ".[dev]" "transformers==5.9" tokenizers
run: pip install -e ".[dev]" "transformers==5.12.1" tokenizers

- name: Remove existing onnxruntime before installing nightly
run: pip uninstall -y onnxruntime onnxruntime-gpu onnxruntime-directml onnxruntime-openvino 2>/dev/null || true
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/trained_tiny_llm_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
matrix:
os: ["ubuntu-latest"]
python-version: ["3.13"]
transformers-version: ["5.6", "5.9"]
transformers-version: ["5.6", "5.12.1"]
torch:
- version: "2.12.0"
steps:
Expand Down
40 changes: 37 additions & 3 deletions modelbuilder/builders/qwen.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ def __init__(self, config, io_dtype, onnx_dtype, ep, cache_dir, extra_options):

self.input_names["position_ids"] = "position_ids"

# When True, the graph input is 2D position_ids [B, S] (as fed by the
# ORT-GenAI ``phi3v`` loader) which is expanded to 3D [3, B, S] inside
# the graph so mRoPE works unchanged. When False (default) the pipeline
# provides 3D position_ids directly.
self.expand_position_ids = False
self.position_ids_reformatted = self.input_names["position_ids"]

self.mrope_sections = self.rope_attrs.get("mrope", {}).get("sections", [])
if not self.mrope_sections:
raise ValueError("MRoPE sections not found in config.text_config.rope_scaling.mrope_section")
Expand Down Expand Up @@ -118,12 +125,34 @@ def make_inv_freq_tensor(self):
print("Created and saved 'model.inv_freq' initializer.")

def make_inputs_and_outputs(self):
# Qwen2.5-VL uses 3D position_ids
self.input_shapes["position_ids"] = [3, "batch_size", "sequence_length"]
# Qwen2.5-VL uses 3D position_ids. When driven by the ORT-GenAI phi3v
# loader (Qwen2.5-Omni) the runtime provides standard 2D position_ids
# which are expanded to 3D inside the graph.
if self.expand_position_ids:
self.input_shapes["position_ids"] = ["batch_size", "sequence_length"]
else:
self.input_shapes["position_ids"] = [3, "batch_size", "sequence_length"]

# Call the base Model's make_inputs_and_outputs (skipping MistralModel's)
super().make_inputs_and_outputs()

def make_preprocessing_nodes(self):
super().make_preprocessing_nodes()
if self.expand_position_ids:
# The graph input is 2D position_ids [B, S]. Expand to 3D [3, B, S]
# for mRoPE by stacking 3 copies.
pos_2d = self.input_names["position_ids"]
unsq_name = "/model/position_ids_expand/Unsqueeze"
unsq_output = f"{unsq_name}/output_0"
self.make_unsqueeze(unsq_name, [pos_2d, "/model/constants/INT64/[0]"], ir.DataType.INT64, [1, "batch_size", "sequence_length"])
tile_name = "/model/position_ids_expand/Tile"
self.make_tile(
tile_name, [unsq_output, "/model/constants/INT64/[3, 1, 1]"], ir.DataType.INT64, [3, "batch_size", "sequence_length"]
)
self.position_ids_reformatted = f"{tile_name}/output_0"
else:
self.position_ids_reformatted = self.input_names["position_ids"]

def make_dynamic_rope_caches(self, layer_id, basename):
# Make nodes for the Dynamic RoPE Cache subgraph
#
Expand Down Expand Up @@ -156,7 +185,7 @@ def make_dynamic_rope_caches(self, layer_id, basename):
# Mul Mul
# (apply scaling) (apply scaling)
#
pos_ids_name = self.input_names["position_ids"]
pos_ids_name = self.position_ids_reformatted
inv_freq_name = "model.inv_freq"
head_dim_half = self.head_size // 2

Expand Down Expand Up @@ -673,6 +702,11 @@ def __init__(self, config, io_dtype, onnx_dtype, ep, cache_dir, extra_options):
# ORT-GenAI uses for this mRoPE model family.
self.model_type = "Qwen2_5_VLForConditionalGeneration"

# The Omni multimodal pipeline is driven by the ORT-GenAI phi3v loader,
# which feeds standard 2D position_ids [B, S]. Expand them to 3D inside
# the graph so the shared mRoPE implementation works unchanged.
self.expand_position_ids = True

def load_weights(self, input_path):
# For quantized models or GGUF use the base class logic.
if self.quant_type is not None or input_path.endswith(".gguf"):
Expand Down
18 changes: 14 additions & 4 deletions modelbuilder/ext_test_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,7 @@ def run_mrope_vl_prefill_and_decode_check(
rtol=None,
seq_len=5,
batch_size=1,
onnx_position_ids_2d=False,
):
"""Run prefill and decode discrepancy checks for VL models.

Expand All @@ -536,8 +537,12 @@ def run_mrope_vl_prefill_and_decode_check(
* ``"inputs_embeds"`` – PyTorch is called with ``inputs_embeds``,
``position_ids``, and ``attention_mask`` (used by Qwen2.5-VL).

The ONNX model always receives ``inputs_embeds`` and a 3-D
``position_ids`` tensor of shape ``[3, batch_size, seq_len]``.
The ONNX model always receives ``inputs_embeds``. ``position_ids`` is a
3-D tensor of shape ``[3, batch_size, seq_len]`` unless
*onnx_position_ids_2d* is ``True`` (Qwen2.5-Omni / phi3v pipeline), in
which case the ONNX model receives standard 2-D ``position_ids`` of
shape ``[batch_size, seq_len]`` and expands them to 3-D internally.
PyTorch always receives the 3-D ``position_ids``.
"""
import torch

Expand All @@ -557,6 +562,8 @@ def run_mrope_vl_prefill_and_decode_check(

# 3D position_ids for mRoPE: [3, batch_size, seq_len]
position_ids_3d = np.tile(np.arange(seq_len, dtype=np.int64), (3, batch_size, 1))
# The ONNX phi3v pipeline (Qwen2.5-Omni) consumes 2D position_ids.
onnx_position_ids = np.tile(np.arange(seq_len, dtype=np.int64), (batch_size, 1)) if onnx_position_ids_2d else position_ids_3d

prefill_results = None
pt_prefill = None
Expand All @@ -565,7 +572,7 @@ def run_mrope_vl_prefill_and_decode_check(
prefill_feed = {
"inputs_embeds": inputs_embeds.cpu().numpy().astype(np_dtype),
"attention_mask": np.ones((batch_size, seq_len), dtype=np.int64),
"position_ids": position_ids_3d,
"position_ids": onnx_position_ids,
}
for i in range(num_hidden_layers):
prefill_feed[f"past_key_values.{i}.key"] = np.zeros((batch_size, num_key_value_heads, 0, head_size), dtype=np_dtype)
Expand Down Expand Up @@ -609,11 +616,12 @@ def run_mrope_vl_prefill_and_decode_check(

# 3D position_ids for decode step: [3, batch_size, 1] with value = seq_len
decode_position_ids_3d = np.full((3, batch_size, 1), seq_len, dtype=np.int64)
onnx_decode_position_ids = np.full((batch_size, 1), seq_len, dtype=np.int64) if onnx_position_ids_2d else decode_position_ids_3d

decode_feed = {
"inputs_embeds": decode_embeds.cpu().numpy().astype(np_dtype),
"attention_mask": np.ones((batch_size, seq_len + 1), dtype=np.int64),
"position_ids": decode_position_ids_3d,
"position_ids": onnx_decode_position_ids,
}
for i in range(num_hidden_layers):
decode_feed[f"past_key_values.{i}.key"] = prefill_results[f"present.{i}.key"]
Expand Down Expand Up @@ -945,6 +953,7 @@ def run_vl_random_weights_test(
atol: Optional[Dict] = None,
rtol: Optional[Dict] = None,
pt_mode: str = "input_ids",
onnx_position_ids_2d: bool = False,
):
"""Build and export a random-weight VL model to ONNX and compare PyTorch vs ONNX.

Expand Down Expand Up @@ -1007,6 +1016,7 @@ def run_vl_random_weights_test(
pt_mode=pt_mode,
atol=atol,
rtol=rtol,
onnx_position_ids_2d=onnx_position_ids_2d,
)

def run_greedy_generation_test(
Expand Down
8 changes: 6 additions & 2 deletions tests/fast/test_random_qwen2_5_omni.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def common_fast_qwen25omni_random_weights(self, precision, provider):
vocab_size=config.text_config.vocab_size,
create_model_kwargs={"num_hidden_layers": num_hidden_layers},
pt_mode="inputs_embeds",
onnx_position_ids_2d=True,
)

@hide_stdout()
Expand Down Expand Up @@ -364,11 +365,14 @@ def common_qwen25omni_conditional_generation(self, precision, provider):
num_kv_heads = config.text_config.num_key_value_heads
head_size_text = config.text_config.hidden_size // config.text_config.num_attention_heads

position_ids_3d = np.tile(np.arange(seq_len, dtype=np.int64), (3, batch_size, 1))
# The Omni text decoder accepts standard 2D position_ids [B, S]
# (as fed by the ORT-GenAI phi3v loader) and expands them to the 3D
# mRoPE layout internally.
position_ids_2d = np.tile(np.arange(seq_len, dtype=np.int64), (batch_size, 1))
onnx_feed = {
"inputs_embeds": inputs_embeds,
"attention_mask": np.ones((batch_size, seq_len), dtype=np.int64),
"position_ids": position_ids_3d,
"position_ids": position_ids_2d,
}
for i in range(num_hidden_layers):
onnx_feed[f"past_key_values.{i}.key"] = np.zeros((batch_size, num_kv_heads, 0, head_size_text), dtype=np_dtype)
Expand Down
Loading