From 4fe2b30be1f93d5fa7a1fb58ad21738a6e42cedf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:53:40 +0000 Subject: [PATCH 1/4] Initial plan From 3fbfbbf7a042d56e9899ee5a6f950663d0fdc337 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:54:56 +0000 Subject: [PATCH 2/4] Switch CI to use transformers==5.12.1 --- .github/workflows/fast_tests.yml | 2 +- .github/workflows/fast_tests_ort_nightly.yml | 2 +- .github/workflows/trained_tiny_llm_tests.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/fast_tests.yml b/.github/workflows/fast_tests.yml index e821260..b4a630a 100644 --- a/.github/workflows/fast_tests.yml +++ b/.github/workflows/fast_tests.yml @@ -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: diff --git a/.github/workflows/fast_tests_ort_nightly.yml b/.github/workflows/fast_tests_ort_nightly.yml index e297ef5..8042130 100644 --- a/.github/workflows/fast_tests_ort_nightly.yml +++ b/.github/workflows/fast_tests_ort_nightly.yml @@ -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 diff --git a/.github/workflows/trained_tiny_llm_tests.yml b/.github/workflows/trained_tiny_llm_tests.yml index 402c764..f509ec5 100644 --- a/.github/workflows/trained_tiny_llm_tests.yml +++ b/.github/workflows/trained_tiny_llm_tests.yml @@ -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: From 1e93e5e73469fcbbb449f805331bb9794ae02647 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Jun 2026 18:07:06 +0000 Subject: [PATCH 3/4] Fix Qwen2.5-Omni genai pipeline to use 2D position_ids (phi3v) --- modelbuilder/builders/qwen.py | 40 ++++++++++++++++++++++++-- modelbuilder/ext_test_case.py | 18 +++++++++--- tests/fast/test_random_qwen2_5_omni.py | 8 ++++-- 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/modelbuilder/builders/qwen.py b/modelbuilder/builders/qwen.py index 087daaf..94b0618 100644 --- a/modelbuilder/builders/qwen.py +++ b/modelbuilder/builders/qwen.py @@ -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") @@ -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 # @@ -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 @@ -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"): diff --git a/modelbuilder/ext_test_case.py b/modelbuilder/ext_test_case.py index 13f54e8..faf4322 100644 --- a/modelbuilder/ext_test_case.py +++ b/modelbuilder/ext_test_case.py @@ -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. @@ -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 @@ -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 @@ -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) @@ -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"] @@ -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. @@ -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( diff --git a/tests/fast/test_random_qwen2_5_omni.py b/tests/fast/test_random_qwen2_5_omni.py index 0fb0df1..bd34a72 100644 --- a/tests/fast/test_random_qwen2_5_omni.py +++ b/tests/fast/test_random_qwen2_5_omni.py @@ -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() @@ -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) From f38341cd084358de1b7ab0e192523dfcdb8c3757 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 08:50:05 +0000 Subject: [PATCH 4/4] Disambiguate trained-tiny CI job name to fix duplicate check-run status reporting --- .github/workflows/trained_tiny_llm_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/trained_tiny_llm_tests.yml b/.github/workflows/trained_tiny_llm_tests.yml index f509ec5..ff51941 100644 --- a/.github/workflows/trained_tiny_llm_tests.yml +++ b/.github/workflows/trained_tiny_llm_tests.yml @@ -11,7 +11,7 @@ permissions: jobs: tests: - name: ci (${{ matrix.os }}, py${{ matrix.python-version }}, torch-${{ matrix.torch.version || 'stable' }}, transformers-${{ matrix.transformers-version || 'latest' }}) + name: ci-trained (${{ matrix.os }}, py${{ matrix.python-version }}, torch-${{ matrix.torch.version || 'stable' }}, transformers-${{ matrix.transformers-version || 'latest' }}) runs-on: ${{ matrix.os }} permissions: contents: read